给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),并返回其最大和。
子数组是数组中的一个连续部分。
示例 1
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:
[4,-1,2,1]
的和最大:
4 + (-1) + 2 + 1 = 6
示例 2
输入:nums = [1]
输出:1
一、暴力解法
最直接的思路是枚举每一个连续子数组。
使用 i 确定子数组的起点,再使用 j 向右扩展:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int ans = nums[0];
for (int i = 0; i < nums.size(); i++) {
int tmp = 0;
for (int j = i; j < nums.size(); j++) {
tmp += nums[j];
ans = max(ans, tmp);
}
}
return ans;
}
};
例如:
nums = [1,2,3]
枚举的连续子数组为:
i = 0:
[1]
[1,2]
[1,2,3]
i = 1:
[2]
[2,3]
i = 2:
[3]
这种方法需要两层循环,因此时间复杂度为:
O(n²)
当数组很大时,会超出时间限制。
二、Kadane 算法
这道题可以优化到 O(n)。
核心思想是:
对于当前位置
nums[i],我们只需要判断:是接在前面的连续子数组后面,还是从当前数字重新开始。
定义:
current
表示:
以当前位置结尾的最大连续子数组和。
那么对于 nums[i],只有两种情况。
情况 1:接着前面的子数组
current + nums[i]
情况 2:从当前位置重新开始
nums[i]
所以:
current = max(nums[i], current + nums[i]);
然后再用 ans 保存整个过程中出现过的最大值:
ans = max(ans, current);
三、为什么可以舍弃前面的数字?
例如:
nums = [4, -10, 6]
走到 -10 时:
[4,-10] = -6
所以:
current = -6
接下来遇到 6。
如果继续带着前面的部分:
-6 + 6 = 0
如果直接从 6 开始:
6
显然:
6 > 0
所以前面的 [4,-10] 可以全部舍弃。
也就是说:
如果前面一段的和已经是负数,那么它只会拖累后面的结果。
因此:
前面的和 > 0
→ 保留,能够给后面的结果加分
前面的和 < 0
→ 舍弃,只会给后面的结果减分
四、为什么不需要"后悔"之前的选择?
关键原因是题目要求的是:
连续子数组。
假设:
nums = [5, -2, 3]
走到 -2 时,有两个可能:
[-2] = -2
[5,-2] = 3
显然:
3 > -2
因此只需要保留:
[5,-2]
为什么可以直接放弃 [-2]?
因为以后无论再接什么数字 x:
3 + x > -2 + x
例如后面接 3:
[5,-2,3] = 6
[-2,3] = 1
原本较差的方案以后接上相同的数字,也永远不可能超过原本较好的方案。
因此只需要保留:
以当前位置结尾的最优方案。
这也是动态规划中的一个重要思想。
五、最终代码
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int current = nums[0];
int ans = nums[0];
for (int i = 1; i < nums.size(); i++) {
// 选择:
// 1. 从 nums[i] 重新开始
// 2. 接在前面的连续子数组后面
current = max(nums[i], current + nums[i]);
// 更新全局最大值
ans = max(ans, current);
}
return ans;
}
};
六、代码理解
最关键的一句:
current = max(nums[i], current + nums[i]);
可以理解为:
当前这个数字,是自己重新开一个子数组,
还是继续接在前面的子数组后面?
而:
ans = max(ans, current);
表示:
记录整个过程中出现过的最大连续子数组和。
七、复杂度分析
Kadane 算法只遍历数组一次:
时间复杂度:O(n)
空间复杂度:O(1)
相比暴力解法:
暴力解法:O(n²)
Kadane: O(n)
效率得到了很大提升。
总结
这道题的关键不是去枚举所有连续子数组,而是维护:
以当前位置结尾的最大连续子数组和
每到一个新的数字,只需要判断:
current = max(nums[i], current + nums[i]);
即:
前面的结果有帮助 → 接着用
前面的结果拖后腿 → 从当前重新开始
最终通过:
ans = max(ans, current);
得到整个数组中的最大连续子数组和。