什么是 Kadane 算法?
Kadane 算法是解决最大子数组和问题的经典算法,由 Jay Kadane 于 1984 年提出。它能在 时间、 空间内找到数组中的最大连续子数组和。
它的核心代码极为简短:
def max_subarray(nums): ans = float('-inf') pre = 0 for x in nums: pre = max(pre, 0) + x ans = max(ans, pre) return ans寥寥几行,却浓缩了动态规划和贪心两种思想的精髓。接下来我们逐步拆解它背后的原理。
核心直觉
想象你是一个股民,每天都有盈亏。你需要在某一天买入、之后某一天卖出,获得最大收益——这就是著名的最大利润问题,本质上就是最大子数组和。
Kadane 算法的核心直觉可以浓缩为一句话:
💡 核心直觉:在遍历数组时,我们始终面临一个选择——是延续之前的累加(pre + x),还是重新开始(x)。做这个选择的依据很简单:如果之前的累加是正数,它对我们有帮助,保留它;如果之前的累加是负数或零,它只会拖后腿,丢弃它。
🧠 换个角度:一个负数的前缀,就像背着一个越来越重的包袱爬山——越早扔掉它,越早轻装上阵。Kadane 算法的
max(pre, 0)就是这个”扔包袱”的动作。
从动态规划的角度理解
状态定义
这是理解 Kadane 算法最关键的一步。定义:
为什么强调”以 结尾”?因为这个限制让子问题之间可以递推——如果我知道以 结尾的最优解,我就能推导出以 结尾的最优解。
💡 设计 DP 的通用技巧:当问题问”任意区间的最优值”时,一个常见的套路是强制让状态以当前位置结尾,然后在所有 中取最大值。这让我们获得了”前一个状态 + 当前元素”的递推关系。类似的设计在「最大乘积子数组」「最长上升子序列」等题目中反复出现。
状态转移
对于位置 ,我们只有两种选择:
选择一:自成一派。 从 开始一个全新的子数组。此时子数组和为 。
选择二:延续前朝。 把 接到以 结尾的最大子数组后面。此时和为 。
取两者中的较大值:
最终答案就是 。
🧠 直觉验证:假设你在位置 3,前面的最大子数组和是 5,当前元素是 -2。延续得到 3,自成一派得到 -2。显然 3 > -2,所以延续。如果当前元素是 -10 呢?延续得到 -5,自成一派得到 -10。还是延续。只有当前面的和本身就是负数时,自成一派才更优——因为负前缀只会让当前元素更小。
空间优化
观察转移方程: 仅依赖于 ,我们不需要维护整个 dp 数组。用一个变量 pre 代替 :
pre = max(x, pre + x)这已经是 空间了。但我们还能再进一步——
注意到:当 时,(因为一个非正数加上 不可能大于 )。所以我们可以将条件前置:
这就是 Kadane 算法的最终形态。语义变成了「如果之前和是正的,加上当前值;如果是负的,丢弃,从当前值重新开始」。
pre = max(pre, 0) + x两种写法完全等价,但后者更直观地体现了”丢弃负前缀”的贪心决策。
从贪心角度理解
换个视角,Kadane 算法也可以看作一种在线贪心:
🧠 贪心视角:我们一边扫描数组,一边维护”当前正在考虑的子数组”。每遇到一个新元素,我们就问自己——这个元素加入后,子数组和是变大了还是变小了?如果变小了(前缀和为负),说明前面的元素全是累赘,“咔嚓”一刀切断,从当前元素重新开始。
这里的贪心选择是局部最优的——在每一步,我们都选择”使当前子数组和最大”的决策。但这之所以能找到全局最优,是因为任何全局最大子数组不可能包含一个和为负的前缀(否则去掉这个前缀会得到一个更大的子数组)。
💡 贪心安全性的证明:假设全局最大子数组是 ,其中 之前有一个和为负的前缀 。那么 的和一定大于 的和(因为负前缀拉低了总和)。因此 Kadane 算法在 位置”丢弃”前一个负前缀的决策,恰好保留了真正的最优解。
为什么 Kadane 算法同时属于 DP 和贪心?
这不是巧合。很多优秀的算法都跨越了多种设计范式:
| 维度 | DP 视角 | 贪心视角 |
|---|---|---|
| 决策依据 | 如果 则保留,否则丢弃 | |
| 状态 | 以 结尾的最大子数组和 | 当前的累积和 |
| 正确性来源 | 最优子结构 | 贪心选择性质 |
| 典型问法 | ”什么是以 i 结尾的最优解?" | "这个前缀对我有没有帮助?” |
Kadane 算法之所以能被两种范式同时解释,是因为它的贪心选择恰好也是 DP 的最优子结构——“丢弃负前缀”这个贪心动作,在 DP 的转移方程中被验证为总是最优的。
💡 什么时候贪心和 DP 重合? 当 DP 的状态转移只涉及”取或不取”的二元选择,且其中一种选择总是被另一种支配时(如”加负数前缀”总是被”丢弃它重新开始”支配),DP 就退化为了贪心。Kadane 正是这类”DP 退化为贪心”的经典案例。
执行过程可视化
以 nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] 为例,追踪 Kadane 算法的每一步决策:
i | x | 前一步 pre | 决策 | 新 pre | ans |
|---|---|---|---|---|---|
| — | — | — | — | 0 | -∞ |
| 0 | -2 | 0 (≤0) | 丢弃,0 + (-2) = -2 | -2 | -2 |
| 1 | 1 | -2 (≤0) | 丢弃,0 + 1 = 1 | 1 | 1 |
| 2 | -3 | 1 (>0) | 保留,1 + (-3) = -2 | -2 | 1 |
| 3 | 4 | -2 (≤0) | 丢弃,0 + 4 = 4 | 4 | 4 |
| 4 | -1 | 4 (>0) | 保留,4 + (-1) = 3 | 3 | 4 |
| 5 | 2 | 3 (>0) | 保留,3 + 2 = 5 | 5 | 5 |
| 6 | 1 | 5 (>0) | 保留,5 + 1 = 6 | 6 | 6 ✅ |
| 7 | -5 | 6 (>0) | 保留,6 + (-5) = 1 | 1 | 6 |
| 8 | 4 | 1 (>0) | 保留,1 + 4 = 5 | 5 | 6 |
注意 三个位置都触发了”丢弃”:
- :前面是 0(等效于没有前缀),取
-2。 - :
pre = -2,是负数,丢弃前面的[-2],从1重新开始。 - :
pre = -2,是负数,丢弃前面的[1, -3],从4重新开始。而 到 连续保留,构建出最优解 (和为 6)。
💡 这个表揭示了 Kadane 算法的运作规律:“丢弃”多发生在遇到负数前缀或数组开头的地方。一旦
pre转正,算法就会贪婪地一直累加(即使遇到负数也加,因为正前缀总体上还是赚的),直到前缀和再次归零或转负。
变体与扩展
记录子数组本身
如果不仅要求和,还要返回具体的子数组怎么办?只需追踪起止索引:
def max_subarray_with_indices(nums): ans = float('-inf') pre = 0 start = end = 0 # 最优子数组的起止位置 cur_start = 0 # 当前 pre 的起点
for i, x in enumerate(nums): if pre <= 0: pre = x cur_start = i # 丢弃前缀,重新标记起点 else: pre += x
if pre > ans: ans = pre start = cur_start end = i
return nums[start:end + 1], ans💡 技巧:每次
pre <= 0触发”丢弃”时,同步更新cur_start为当前位置。当pre刷新历史最高记录时,将cur_start封存到start。这样在 额外空间内就能追踪子数组边界。
乘积最大子数组
将加法换成乘法后,负数的存在让问题变得更有趣——两个负数相乘会变成正数,所以当前的负数可能是未来的宝藏。这个变体需要同时维护”以 结尾的最大乘积”和”以 结尾的最小乘积”两个状态。详见 乘积最大子数组题解。
环形子数组的最大和
如果数组是环形的(首尾相连),最大子数组可能跨越数组两端。诀窍在于:环形最大 = max(普通最大, 总和 - 普通最小)。因为跨越两端的子数组,等价于”挖掉中间一段最小的”。详见 环形子数组的最大和。
线段树维护区间最大子段和
当数组需要频繁修改,并且每次修改后要快速回答”整个数组的最大子段和是多少”时,Kadane 的 单次查询就不够用了。这时需要线段树来维护区间信息——而线段树节点的合并操作,正是 Kadane 算法分治视角的推广。
总结
Kadane 算法虽然只有短短几行代码,但它身上凝聚了几个非常重要的算法设计思想:
- 强制结尾的 DP 状态定义:,这是子数组类 DP 的万能钥匙。
- 空间优化的来源识别: 只依赖 → 滚动变量 → 空间。
- DP 退化为贪心:当状态转移中的一种选择被另一种严格支配时,DP 可以简化为贪心决策。
- 分治视角:跨越中点的最大和 = 左半后缀最大 + 右半前缀最大,这直接导向线段树的节点合并。
💡 推荐练习顺序:最大子数组和(本题)→ 乘积最大子数组 → 环形子数组的最大和 → 线段树区间最大子段和。这个序列将 Kadane 算法的思想逐步泛化,覆盖了 DP、贪心、分治、线段树四个领域。
掌握了 Kadane 算法,你掌握的不仅仅是一道题,而是一整套处理”子数组最值”问题的思维框架。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









