最大子数组和是一道非常经典的题。
经典到什么程度?
它几乎是:
- 分治思想的标准示例
- 动态规划入门必刷题
- 后续连续区间问题的“母题”
这篇笔记不只给答案,而是完整走一遍思路升级的过程。
一、题目要求
给定一个整数数组 nums,找到一个连续子数组(至少包含一个元素),使其元素和最大,返回这个最大和。
示例:
输入:[-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:[4,-1,2,1] 的和最大
关键约束:
- 子数组必须连续
- 数组中可能全部是负数
二、从最直观的思考开始
如果不考虑效率,最直接的想法是:
- 枚举所有子数组
- 计算它们的和
- 取最大值
时间复杂度是 O(n²),显然不可取。
那有没有一种方式,在结构上拆解这个问题?
答案是:有,分治。
三、分治法:区间三分的经典思想
核心思想
对于任意一个区间 [left, right],最大子数组只可能来自三种情况:
- 完全在左半区间
- 完全在右半区间
- 横跨中点
mid
这就是分治的基础。
分治函数定义
getMax(nums, left, right)
含义:
返回 nums[left..right] 区间内的最大子数组和
递归终止条件
if (left == right) {
return nums[left];
}
区间里只有一个数,答案只能是它本身。
分治完整代码
class Solution {
public int maxSubArray(int[] nums) {
return getMax(nums, 0, nums.length - 1);
}
public int getMax(int[] nums, int left, int right) {
if (left == right) {
return nums[left];
}
int mid = left + (right - left) / 2;
int leftMax = getMax(nums, left, mid);
int rightMax = getMax(nums, mid + 1, right);
int crossMax = getCrossMax(nums, left, right);
return Math.max(leftMax, Math.max(rightMax, crossMax));
}
public int getCrossMax(int[] nums, int left, int right) {
int mid = left + (right - left) / 2;
int leftSum = nums[mid];
int leftMax = leftSum;
for (int i = mid - 1; i >= left; i--) {
leftSum += nums[i];
leftMax = Math.max(leftSum, leftMax);
}
int rightSum = nums[mid + 1];
int rightMax = rightSum;
for (int i = mid + 2; i <= right; i++) {
rightSum += nums[i];
rightMax = Math.max(rightSum, rightMax);
}
return leftMax + rightMax;
}
}
分治法在做什么?
- 左递归:左区间的最大子数组
- 右递归:右区间的最大子数组
- 中间扫描:以
mid为分界的最大跨区间子数组
这是一个逻辑非常严谨、思想非常漂亮的解法。
分治法的代价
时间复杂度:
T(n) = 2T(n/2) + O(n) = O(n log n)
这已经不错了,但还能更好。
四、动态规划:从“区间”转向“当前位置”
换一个角度思考:
如果我已经走到位置 i,
那“以 i 结尾的最大子数组和”是多少?
这就是动态规划的切入点。
状态定义
dp[i] 表示:以 nums[i] 结尾的最大子数组和
注意:
必须 以 i 结尾,这是关键。
状态转移方程
来到第 i 个元素时,只有两种选择:
- 把 nums[i] 接在前一个子数组后面
- 从 nums[i] 自己重新开始
公式就是:
dp[i] = max(dp[i - 1] + nums[i], nums[i])
五、动态规划标准写法(使用 dp 数组)
完整代码
class Solution {
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
int res = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
res = Math.max(res, dp[i]);
}
return res;
}
}
逐行理解
dp[i - 1] + nums[i]
表示“之前的子数组是正收益,继续扩展”nums[i]
表示“之前是负担,直接丢掉,从当前重新开始”
六、动态规划的空间优化
观察转移方程可以发现:
dp[i] 只依赖 dp[i - 1]
所以数组是可以压缩的。
空间优化版代码
class Solution {
public int maxSubArray(int[] nums) {
int preMax = nums[0];
int res = preMax;
for (int i = 1; i < nums.length; i++) {
int curMax = Math.max(nums[i], preMax + nums[i]);
res = Math.max(res, curMax);
preMax = curMax;
}
return res;
}
}
这一步优化的本质
preMax相当于dp[i - 1]curMax相当于dp[i]- 状态没有变,只是存储方式变了
七、三种思路的对比
| 解法 | 时间复杂度 | 空间复杂度 | 作用 |
|---|---|---|---|
| 分治法 | O(n log n) | O(log n) | 理解区间思想 |
| DP 数组 | O(n) | O(n) | 推导状态转移 |
| DP 优化 | O(n) | O(1) | 实战首选 |
八、总结
这道题非常适合用来建立一个清晰认知:
- 分治告诉你:
答案可以从区间结构中拆出来 - 动态规划告诉你:
当前位置的选择只和上一步有关 - 空间优化告诉你:
不是所有状态都值得被保存
如果你能把这三种解法串起来理解,
后面再遇到连续子数组、区间最值、股票类问题,
你会发现它们本质上都在做同一件事。