前言
"最大子数组和"是数组与动态规划中的经典题目。它的代码只有几行,但真正重要的是理解:当我们遍历到一个新元素时,什么时候应该延续前面的子数组,什么时候应该从当前元素重新开始。
这道题对应 LeetCode 53,核心解法通常称为 Kadane 算法。它既可以从动态规划角度理解,也可以看作一种基于局部最优决策的贪心过程。
一、题目说明
给定一个整数数组 nums,找出一个具有最大和的连续子数组,并返回该子数组的元素和。子数组至少包含一个元素。
例如:
输入:nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
输出:6
解释:连续子数组 [4, -1, 2, 1] 的和最大,为 6。
这里最重要的限制是"连续"。我们不能像子序列问题一样跳过中间元素。
二、暴力枚举为什么不够好?
最直观的办法是枚举所有子数组的起点和终点,再计算每个区间的和。如果使用前缀和优化区间求和,仍然需要枚举两个端点,时间复杂度为 O(n²);如果每次重新累加,复杂度甚至可能达到 O(n³)。
实际上,我们没有必要反复计算每个区间。遍历到当前元素时,只需要知道一个信息:以前一个位置结尾的最大子数组和,是否值得继续保留?
三、Kadane 算法的核心思想
定义两个变量:
-
current:以当前元素结尾的最大子数组和; -
result:遍历过程中出现过的全局最大子数组和。
对于当前元素 nums[i],只有两种选择:
-
接在前面的子数组后面,得到
current + nums[i]; -
放弃前面的子数组,从当前元素重新开始,得到
nums[i]。
因此状态转移公式为:
current = max(nums[i], current + nums[i])
result = max(result, current)
为什么前面的和小于零时可以直接放弃?假设此前的子数组和为 -3,当前元素为 4:
-3 + 4 = 1
显然不如直接从 4 开始。任何负数前缀都会拖累后面的结果,所以它不可能成为最优子数组的一部分。
反过来,如果此前的和为正数,它能够增大当前元素构成的子数组和,就应该继续保留。这个判断就是 Kadane 算法能够一次遍历完成计算的关键。
四、示例演算
以数组 [-2, 1, -3, 4, -1, 2, 1, -5, 4] 为例:
| 当前元素 | current:以当前位置结尾的最大和 |
result:全局最大和 |
|---|---|---|
| -2 | -2 | -2 |
| 1 | 1 | 1 |
| -3 | -2 | 1 |
| 4 | 4 | 4 |
| -1 | 3 | 4 |
| 2 | 5 | 5 |
| 1 | 6 | 6 |
| -5 | 1 | 6 |
| 4 | 5 | 6 |
遍历到 4 时,前面的 current 为 -2,继续累加只会拖累结果,因此从 4 重新开始。随后依次加入 -1、2、1,最终得到最大和 6。
注意,遍历到 -5 时,current 虽然下降为 1,但仍然是正数,所以理论上仍有保留价值。最终加入 4 得到 5,没有超过全局最大值 6。
五、Java 代码实现
1. 标准动态规划写法
class Solution {
public int maxSubArray(int[] nums) {
int current = nums[0];
int result = nums[0];
for (int i = 1; i < nums.length; i++) {
current = Math.max(nums[i], current + nums[i]);
result = Math.max(result, current);
}
return result;
}
}
这段代码直接对应状态转移公式,含义也最清晰:当前元素要么加入原子数组,要么成为新子数组的起点。
2. 等价写法
class Solution {
public int maxSubArray(int[] nums) {
int result = nums[0];
int sum = 0;
for (int num : nums) {
if (sum > 0) {
sum += num;
} else {
sum = num;
}
result = Math.max(result, sum);
}
return result;
}
}
两种写法本质完全相同:
sum > 0 → 延续原子数组
sum <= 0 → 从当前元素重新开始
标准写法更方便推导动态规划公式;截图写法则更加直观地体现了"负数前缀立即舍弃"的思想。
六、复杂度分析
-
时间复杂度:
O(n)。只需要遍历数组一次; -
空间复杂度:
O(1)。只使用两个变量,没有额外数组。
如果使用完整的动态规划数组,令 dp[i] 表示以 nums[i] 结尾的最大子数组和,也可以求解,但每个状态只依赖前一个状态,因此可将空间从 O(n) 优化为 O(1)。