最大子数组和 图解题解
这道题到底在问什么
- 输入
- nums=[-2,1,-3,4,-1,2,1,-5,4]
- 输出
- 6
最优解:为什么这么做
一句话答案:LeetCode 53 最大子数组和的最优解是动态规划,也就是经典的 Kadane 算法:定义「必须以第 i 个数结尾」的最大和 cur,每到一个新数只做一次抉择——前面那段的和为正就接上、为负就丢掉另起炉灶,同时用 best 记录全程出现过的最大值。一遍扫描搞定,时间 O(n)、空间 O(1)。
最大子数组和这道题在问什么
给一个整数数组 nums(有正有负),找一个连续的子数组,至少包含一个元素,使它的元素和最大,返回这个和。「连续」是全部难度的来源:如果允许任选元素,把正数全挑出来就完了;正因为必须连成一片,才会出现「要不要为了后面的大正数,忍着带上中间几个负数」这种权衡。
为什么暴力枚举是 O(n²),慢在哪
暴力做法是枚举子数组的起点和终点,配合前缀和可以把每段的和算到 O(1),但起终点组合仍有约 n²/2 种,整体 O(n²)。慢的根源是重复权衡:以位置 i 结尾的各段和,与以 i+1 结尾的各段和之间只差一个元素,暴力却把它们当成不相干的问题从头算。如果「以 i 结尾的最优」能一步递推出「以 i+1 结尾的最优」,整个问题就只剩一趟线性扫描。
状态为什么要定义成「必须以第 i 个数结尾」
直接定义「前 i 个数里的最大子数组和」是递推不动的:那个最优段可能结束在前面任何位置,跟第 i+1 个数接不接得上说不清,新元素来了无从更新。加一个限制就通了——定义 dp[i] 为「所有以第 i 个数结尾的子数组」里的最大和。锚死了结尾,第 i+1 个数的处境立刻清晰:它要么接在前一段屁股后面,要么自己另起一段,只有这两种可能。
这个定义不会漏掉答案:任何子数组总得在某个位置结尾,把每个位置结尾的最优都算出来,全局最大值就藏在其中。所以最终答案是所有 dp[i] 里的最大值,而不是最后一个 dp——最优段完全可能在数组中间就结束了,示例 [-2,1,-3,4,-1,2,1,-5,4] 的答案 6 对应中段的 [4,-1,2,1],就不在末尾。
前面的和为负就丢掉,为什么是对的
转移写出来是 dp[i] = max(nums[i], dp[i-1] + nums[i]),白话就是「接上前段」与「另起炉灶」二选一。它的正确性一句话可以点透:前段能提供的全部价值就是它的和 dp[i-1]——这个和是正数,接上等于白赚一笔垫底分;是负数,接上只会把自己拖低,还不如从 nums[i] 从零开始。注意被丢弃的判据是「前段的和为负」,不是「前面出现过负数」:示例里 4 后面跟着 -1,因为当时累计和 4 - 1 = 3 仍是正的,继续带着走才吃到了后面的 2 和 1。
由于 dp[i] 只依赖 dp[i-1],连数组都不用开,两个变量就够:cur 滚动记「以当前数结尾的最大和」,best 记全程最大值。这就是 Kadane 算法的全部。
复杂度与最容易踩的边界
时间 O(n):数组只扫一遍,每步一次比较一次加法。空间 O(1):只有 cur、best 两个变量。
最经典的坑是 best 的初值:必须设成 nums[0],不能设 0。数组全是负数时,正确答案是其中最大的那个负数,初值 0 会让你错误地返回 0——而空子数组是不允许的,题目要求至少含一个元素。另一个坑前面已提:答案是扫描全程的最大值,别返回最后一步的 cur。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3关键直觉:如果前面那段的和已经是负的,接上它只会拖累自己,不如丢掉、从这个数重新开始。
- 4上行是原数组 nums(固定),下行 dp 待填——dp[i] 是“到第 i 个数为止、必须以它结尾”的最大和。从最左开始。
- 5只有第 0 个数,子数组只能是它自己:dp[0]=nums[0]=-2。
- 6算 dp[1]:另起一段=nums[1]=1;接上前面=dp[0]+nums[1]=-2+1=-1。前面 dp[0]=-2 是负的,接上反而更小。
- 7取较大的 1 填进 dp[1]。丢掉前面、从这个数另起更划算。
- 8算 dp[2]:另起一段=nums[2]=-3;接上前面=dp[1]+nums[2]=1+-3=-2。前面 dp[1]=1 是正的,接上能加分。
- 9取较大的 -2 填进 dp[2]。接上前面更划算。
- 10算 dp[3]:另起一段=nums[3]=4;接上前面=dp[2]+nums[3]=-2+4=2。前面 dp[2]=-2 是负的,接上反而更小。
- 11取较大的 4 填进 dp[3]。丢掉前面、从这个数另起更划算。
- 12算 dp[4]:另起一段=nums[4]=-1;接上前面=dp[3]+nums[4]=4+-1=3。前面 dp[3]=4 是正的,接上能加分。
- 13取较大的 3 填进 dp[4]。接上前面更划算。
- 14算 dp[5]:另起一段=nums[5]=2;接上前面=dp[4]+nums[5]=3+2=5。前面 dp[4]=3 是正的,接上能加分。
- 15取较大的 5 填进 dp[5]。接上前面更划算。
- 16算 dp[6]:另起一段=nums[6]=1;接上前面=dp[5]+nums[6]=5+1=6。前面 dp[5]=5 是正的,接上能加分。
- 17取较大的 6 填进 dp[6]。接上前面更划算。
- 18算 dp[7]:另起一段=nums[7]=-5;接上前面=dp[6]+nums[7]=6+-5=1。前面 dp[6]=6 是正的,接上能加分。
- 19取较大的 1 填进 dp[7]。接上前面更划算。
- 20算 dp[8]:另起一段=nums[8]=4;接上前面=dp[7]+nums[8]=1+4=5。前面 dp[7]=1 是正的,接上能加分。
- 21取较大的 5 填进 dp[8]。接上前面更划算。
- 22dp 全部填好。答案不是最后一格——要在整行 dp 里横扫一遍找最大的那个,因为最优子数组可以在任何位置结尾。
- 23整行扫下来,最大的是 dp[6]=6——这就是最大子数组和(对应子数组 [4,-1,2,1])。
⚠️ 容易写错的地方
✗ 错:答案取 dp[n-1]
✓ 对:答案是 max(dp)
最优子数组可在任意位置结尾
✗ 错:best 初值设 0
✓ 对:best 初值设 nums[0]
全是负数时答案应是最大的那个负数,不是 0
✗ 错:前段为负还硬接
✓ 对:前段为负就丢掉、重新开始
接负数只会把当前和拖小
完整代码(Python / C++ / Java)
Python
def maxSubArray(nums):
cur = best = nums[0]
for x in nums[1:]:
cur = max(x, cur + x) # 另起 or 接上
best = max(best, cur)
return bestC++
int maxSubArray(vector<int>& nums){
int cur = nums[0], best = nums[0];
for(int i = 1; i < nums.size(); i++){
cur = max(nums[i], cur + nums[i]);
best = max(best, cur);
}
return best;
}Java
int maxSubArray(int[] nums){
int cur = nums[0], best = nums[0];
for(int i = 1; i < nums.length; i++){
cur = Math.max(nums[i], cur + nums[i]);
best = Math.max(best, cur);
}
return best;
}复杂度
时间
O(n)
一遍线性扫描
空间
O(1)
两个滚动变量 cur、best
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最大子数组和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如何同时返回这段子数组的起止下标?+
当 cur 被重置为 nums[i](另起一段)时记下新起点;每次刷新 best 时记下当前终点。
为什么不用前缀和 + 暴力?+
前缀和暴力是 O(n²)。Kadane 把“以 i 结尾的最大和”递推下来,降到 O(n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最大子数组和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。