hot150-14-Kadane算法
难度说明:🟢简单🟡中等🔴困难
hot150-14-Kadane算法
Kadane's算法是一种用于解决最大子数组和问题(Maximum Subarray Sum Problem)的动态规划算法。该问题的目标是在一个给定数组中找到一个连续子数组,使得子数组的和最大。算法的基本思想是通过迭代数组的每个元素,维护两个变量:当前最大子数组和以及全局最大子数组和。在每一步中,都考虑是否将当前元素添加到当前最大子数组和中,或者重新开始一个新的子数组。这样一步步地迭代数组,最终得到全局最大子数组和
🟡01-最大子数组和(53)
1.题目内容
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
2.题解思路
👻方法1:动态规划
- 思路分析:中规中矩按照动态规划的解题步骤进行思考
动态规划解析步骤模板:
- 【步骤1】:确定dp数组及下标含义(根据穷举规律来理解分析)
- 【步骤2】:得到推导公式(结合规律得到
dp[k]
的推导公式,即dp[k]
与前面的元素有什么联系) - 【步骤3】:初始化dp数组(处理初始状态和一些临界的情况)
- 【步骤4】:确定循环(通过循环和推导公式填充dp数组)
- 【步骤5】:验证dp数组正确性
最大子数组之和
- 【1】
dp[i]
设定表示以i
位置元素(nums[i]
)结尾的子数组的最大和 - 【2】推导公式:
dp[i]=max{dp[i-1]+nums[i],nums[i]}
(思考i
和i-1
的关系)- 已知
i-1
位置的最大子数组之和,则对于第i
个位置而言只需要考虑是否要拼在前一个序列后面- 因为此处dp的设定是记录以当前元素结尾的子数组的最大和,因此必须以
nums[i]
结尾,因此此处是对dp[i-1]
的判断,如果它能带来正效益就加入,不能就切断只保留nums[i]
,进而达到状态转移方程
- 因为此处dp的设定是记录以当前元素结尾的子数组的最大和,因此必须以
- 已知
- 【3】基于上述步骤构建dp数组,还需要对这个dp数组中的每个元素进行遍历,进而得到整个数组的最大子数组和
- 优化:也可以在步骤2推导
dp[i]
的遍历过程中同步更新这个max(此处为了语义拆分开来)
- 优化:也可以在步骤2推导
- 【1】
/**
* 53 最大子数组和
*/
public class Solution1 {
/**
* 动态规划思路:
* 1.dp[i] 数组表示以i位置结尾的子数组和的最大值(可以通过累加来更新这个值),初始化
* 2.状态转移方程:dp[i] = max{dp[i-1]+nums[i],nums[i]}
* - dp[i]的选择必须以num[i]结尾,因此其只需要考虑dp[i-1]是否可以给它带来正效益,如果不能就直接切断只保留nums[i]
*/
public int maxSubArray(int[] nums) {
// 1.定义dp数组
int[] dp = new int[nums.length];
dp[0] = nums[0];
// 2.遍历数组元素,更新值
for (int i = 1; i < nums.length; i++) {
// dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
if (dp[i - 1] >= 0) {
dp[i] = dp[i - 1] + nums[i];
} else {
dp[i] = Math.max(dp[i - 1], nums[i]);
}
}
// 3.遍历dp数组,获取到dp数组的最大值
int max = Integer.MIN_VALUE;
for (int i = 0; i < dp.length; i++) {
max = Math.max(max, dp[i]);
}
// 返回结果
return max;
}
}
复杂度分析
时间复杂度:O(n) n 为数组长度,此处的设定为了区分语义需要遍历两次数组(一次num、一次dp)
空间复杂度:O(n)需要构建dp数组存储以
i
位置元素结尾的最大子数组和
👻方法2:Kadane算法(动态规划的空间优化版本)
基于动态规划的思路,可以看到dp[i]
只关注dp[i-1]
和当前的nums[i]
,因此此处可以对原有的动态规划版本进行优化:两个变量
- 【优化1】空间优化:只需要用一个变量
pre
始终指向以上一个元素结尾的最大子数组和 - 【优化2】时间优化:对于
max
的更新也可以同步在循环遍历的过程中进行更新
/**
* 53 最大子数组和
*/
public class Solution3 {
/**
* 动态规划空间优化版本:Kadane算法
*/
public int maxSubArray(int[] nums) {
/**
* 因为dp[i]只和dp[i-1]、nums[i]相关,因此可以只用一个变量始终指向上一个位置的最大连续子数组和,并同步更新max
*/
int preMax = 0, max = Integer.MIN_VALUE;
// 遍历元素,更新
for (int i = 0; i < nums.length; i++) {
preMax = Math.max(preMax + nums[i], nums[i]); // 累加和
max = Math.max(preMax, max);
}
// 返回结果
return max;
}
}
复杂度分析
- 时间复杂度:O(n)n为数组长度,仅需遍历一次
空间复杂度:O(1)仅用两个变量存储元素(常数级别的消耗)
🟡02-环形最大子数组和(918)
1.题目内容
给定一个长度为 n
的环形整数数组 nums
,返回 nums
的非空 子数组 的最大可能和 。
环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i]
的下一个元素是 nums[(i + 1) % n]
, nums[i]
的前一个元素是 nums[(i - 1 + n) % n]
。
子数组 最多只能包含固定缓冲区 nums
中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j]
,不存在 i <= k1, k2 <= j
其中 k1 % n == k2 % n
。
示例 1:
输入:nums = [1,-2,3,-2]
输出:3
解释:从子数组 [3] 得到最大和 3
示例 2:
输入:nums = [5,-3,5]
输出:10
解释:从子数组 [5,5] 得到最大和 5 + 5 = 10
示例 3:
输入:nums = [3,-2,2,-3]
输出:3
解释:从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3
2.题解思路
👻方法1:分段处理
思路分析:分析环形情况下最大子数组和出现的情况
- 【1】拆解出现最大子数组和的两种场景:中间段(和最大子数组和的思路一致)、头尾段(最大前缀和 + 固定后缀和)
- 【2】分别计算这两种场景的最大子数组和(
max1
、max2
),取得最大值
场景分析
- 【场景1】构成最大子数组和的子数组为
nums[i,j]
(共j-i
个元素,元素个数不能超过n) - 【场景2】构成最大子数组和的子数组为
nums[0,i]
+nums[j,n]
- 【场景1】构成最大子数组和的子数组为
上述的第1种情况和【非环形数组求最大子数组和】的情况是一样的,此处继续分析第2种情况:
- 【场景2】中的情况可以拆分为两部分求解:
nums[0,i]
是数组的某一前缀,nums[j,n]
是数组的某一后缀- 遍历时可以通过枚举
j
进而确定sum(nums[j:n])
的值,右边坐标点确定后则可进一步确定[0,j-1]
范围内的最大前缀和,然后将两者相加并进行更新
/**
* 918 环形子数组的最大和(4 ms)
*/
public class Solution3 {
public int maxSubarraySumCircular(int[] nums) {
/**
* 拆分为两部分:
* [场景1]:中间段
* - 思路和单数组求解最大子数组和一致
* [场景2]:头尾段(最大前缀和+固定后缀和)
* - 1.[0,i] 的最大前缀和
* - 2.[j,n] 的固定后缀和
*/
int len = nums.length;
/**
* 场景1:求最大子数组和(Kadane 算法优化版本)
*/
int pre = nums[0]; // pre 指向以上一位置结尾的最大子数组和
int max1 = nums[0]; // max1 记录数组整体的最大子数组和(遍历的时候同步更新)
for(int i=1;i<len;i++){
pre = Math.max(pre+nums[i],nums[i]);
max1 = Math.max(pre,max1);
}
/**
* 场景2:求头(最大前缀和)+尾(固定后缀和)
* 前缀累加和:sum[i] = sum[i-1] + nums[i-1] => res = res + nums[i-1](空间优化)
*/
int[] leftMax = new int[len]; // leftMax[i] 指向当前位置的最大前缀和
int curSum = 0; // 当前累加和
leftMax[0] = 0;
// 1.计算每个位置的最大前缀和 (从第一个元素遍历)
for(int i=1;i<len;i++){
curSum = curSum + nums[i-1];
leftMax[i] = Math.max(leftMax[i-1],curSum);
}
// 2.逆序遍历计算固定后缀和(此处逆序遍历就和上面的正序遍历i保持一致)
int rightSum = 0;
int max2 = Integer.MIN_VALUE;
for(int i=len-1;i>=0;i--){
rightSum = rightSum + nums[i]; // 固定后缀和
max2 = Math.max(max2,rightSum+leftMax[i]); // 更新【场景2】中的最大值
}
// 最终返回max1、max2两种场景的最大的情况
return Math.max(max1,max2);
}
}
优化版本
基于上述分析可以看到,根据解题思路可以得到上述的算法设计,实际上还可以进一步进行时间和空间的优化。例如场景1和场景2(头段部分)实际是对数组元素的一次遍历过程中求的所需值,因此此处可以将这两部分合在一起
- 最大子数组和(Kadane 算法优化版本)
pre
、max
实现空间优化
- 最大前缀和(空间优化)
leftMax[i]
记录当前位置的最大前缀和- 最大前缀和可以通过每次累加后同步更新
sum[i] = sum[i-1] + nums[i-1]
=>res = res + nums[i-1]
(空间优化)
- 最大前缀和可以通过每次累加后同步更新
/**
* 918 环形子数组的最大和
*/
public class Solution4 {
public int maxSubarraySumCircular(int[] nums) {
/**
* 拆分为两部分:
* [场景1]:中间段
* - 思路和单数组求解最大子数组和一致
* [场景2]:头尾段(最大前缀和+固定后缀和)
* - 1.[0,i] 的最大前缀和
* - 2.[j,n] 的固定后缀和
*/
int len = nums.length;
// 场景1:求最大子数组和(Kadane 算法优化版本)所需参数
int pre = nums[0]; // pre 指向以上一位置结尾的最大子数组和
int max1 = nums[0]; // max1 记录数组整体的最大子数组和(遍历的时候同步更新)
// 场景2:求头(最大前缀和)+尾(固定后缀和) 针对头段部分求解所需参数
int[] leftMax = new int[len]; // leftMax[i] 指向当前位置的最大前缀和
int curSum = 0; // 当前累加和
leftMax[0] = 0;
// 遍历数组元素分别进行求解
for (int i = 1; i < len; i++) {
// 求最大子数组和
pre = Math.max(pre + nums[i], nums[i]);
max1 = Math.max(pre, max1);
// 求最大前缀和
curSum = curSum + nums[i - 1]; // sum[i] = sum[i-1] + nums[i-1] => res = res + nums[i-1](空间优化)
leftMax[i] = Math.max(leftMax[i - 1], curSum);
}
// 2.逆序遍历计算固定后缀和(此处逆序遍历就和上面的正序遍历i保持一致)
int rightSum = 0;
int max2 = Integer.MIN_VALUE; // max2 记录场景2中的最大连续环形子数组和
for (int i = len - 1; i >= 0; i--) {
rightSum = rightSum + nums[i]; // 固定后缀和
max2 = Math.max(max2, rightSum + leftMax[i]); // 更新【场景2】中的最大值
}
// 最终返回max1、max2两种场景的最大的情况
return Math.max(max1, max2);
}
}
- 复杂度分析
- 时间复杂度:O(n)遍历数组元素(遍历两遍数组)
- 空间复杂度:O(n)需要存储
leftMax
最大前缀和,且需额外的常数级别空间存储
👻方法2:滑动窗口
思路分析:正常情况下容易联想到通过复制一份数组,然后将环变成单数组的形式来实现(在长度为2n的数组上,寻找长度不超过n的最大子数组和)。然后将题目转化为借助滑动窗口
的思路来实现,需注意这个滑动窗口的大小不是固定的,但最大长度不能超过n