mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
2557 字
7 分钟
Kadane 算法详解
2026-06-30

什么是 Kadane 算法?#

Kadane 算法是解决最大子数组和问题的经典算法,由 Jay Kadane 于 1984 年提出。它能在 O(n)O(n) 时间、O(1)O(1) 空间内找到数组中的最大连续子数组和。

它的核心代码极为简短:

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[i]=以 nums[i] 结尾的最大子数组和dp[i] = \text{以 } nums[i] \text{ 结尾的最大子数组和}

为什么强调”以 ii 结尾”?因为这个限制让子问题之间可以递推——如果我知道以 i1i-1 结尾的最优解,我就能推导出以 ii 结尾的最优解。

💡 设计 DP 的通用技巧:当问题问”任意区间的最优值”时,一个常见的套路是强制让状态以当前位置结尾,然后在所有 dp[i]dp[i] 中取最大值。这让我们获得了”前一个状态 + 当前元素”的递推关系。类似的设计在「最大乘积子数组」「最长上升子序列」等题目中反复出现。

状态转移#

对于位置 ii,我们只有两种选择:

选择一:自成一派。nums[i]nums[i] 开始一个全新的子数组。此时子数组和为 nums[i]nums[i]

选择二:延续前朝。nums[i]nums[i] 接到以 nums[i1]nums[i-1] 结尾的最大子数组后面。此时和为 dp[i1]+nums[i]dp[i-1] + nums[i]

取两者中的较大值:

dp[i]=max(nums[i], dp[i1]+nums[i])dp[i] = \max(nums[i],\ dp[i-1] + nums[i])

最终答案就是 max0i<ndp[i]\max\limits_{0 \le i < n} dp[i]

🧠 直觉验证:假设你在位置 3,前面的最大子数组和是 5,当前元素是 -2。延续得到 3,自成一派得到 -2。显然 3 > -2,所以延续。如果当前元素是 -10 呢?延续得到 -5,自成一派得到 -10。还是延续。只有当前面的和本身就是负数时,自成一派才更优——因为负前缀只会让当前元素更小。

空间优化#

观察转移方程:dp[i]dp[i] 仅依赖于 dp[i1]dp[i-1],我们不需要维护整个 dp 数组。用一个变量 pre 代替 dp[i1]dp[i-1]

pre = max(x, pre + x)

这已经是 O(1)O(1) 空间了。但我们还能再进一步——

注意到:当 pre0pre \le 0 时,max(x,pre+x)=x\max(x, pre + x) = x(因为一个非正数加上 xx 不可能大于 xx)。所以我们可以将条件前置:

pre=max(pre,0)+xpre = \max(pre, 0) + x

这就是 Kadane 算法的最终形态。语义变成了「如果之前和是正的,加上当前值;如果是负的,丢弃,从当前值重新开始」。

pre = max(pre, 0) + x

两种写法完全等价,但后者更直观地体现了”丢弃负前缀”的贪心决策。

从贪心角度理解#

换个视角,Kadane 算法也可以看作一种在线贪心

🧠 贪心视角:我们一边扫描数组,一边维护”当前正在考虑的子数组”。每遇到一个新元素,我们就问自己——这个元素加入后,子数组和是变大了还是变小了?如果变小了(前缀和为负),说明前面的元素全是累赘,“咔嚓”一刀切断,从当前元素重新开始。

这里的贪心选择是局部最优的——在每一步,我们都选择”使当前子数组和最大”的决策。但这之所以能找到全局最优,是因为任何全局最大子数组不可能包含一个和为负的前缀(否则去掉这个前缀会得到一个更大的子数组)。

💡 贪心安全性的证明:假设全局最大子数组是 nums[L..R]nums[L..R],其中 LL 之前有一个和为负的前缀 nums[k..L1]nums[k..L-1]。那么 nums[L..R]nums[L..R] 的和一定大于 nums[k..R]nums[k..R] 的和(因为负前缀拉低了总和)。因此 Kadane 算法在 LL 位置”丢弃”前一个负前缀的决策,恰好保留了真正的最优解。

为什么 Kadane 算法同时属于 DP 和贪心?#

这不是巧合。很多优秀的算法都跨越了多种设计范式:

维度DP 视角贪心视角
决策依据dp[i]=max(x,pre+x)dp[i] = \max(x, pre + x)如果 pre>0pre > 0 则保留,否则丢弃
状态ii 结尾的最大子数组和当前的累积和
正确性来源最优子结构贪心选择性质
典型问法”什么是以 i 结尾的最优解?""这个前缀对我有没有帮助?”

Kadane 算法之所以能被两种范式同时解释,是因为它的贪心选择恰好也是 DP 的最优子结构——“丢弃负前缀”这个贪心动作,在 DP 的转移方程中被验证为总是最优的。

💡 什么时候贪心和 DP 重合? 当 DP 的状态转移只涉及”取或不取”的二元选择,且其中一种选择总是被另一种支配时(如”加负数前缀”总是被”丢弃它重新开始”支配),DP 就退化为了贪心。Kadane 正是这类”DP 退化为贪心”的经典案例。

执行过程可视化#

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] 为例,追踪 Kadane 算法的每一步决策:

ix前一步 pre决策preans
0-∞
0-20 (≤0)丢弃,0 + (-2) = -2-2-2
11-2 (≤0)丢弃,0 + 1 = 111
2-31 (>0)保留,1 + (-3) = -2-21
34-2 (≤0)丢弃,0 + 4 = 444
4-14 (>0)保留,4 + (-1) = 334
523 (>0)保留,3 + 2 = 555
615 (>0)保留,5 + 1 = 666
7-56 (>0)保留,6 + (-5) = 116
841 (>0)保留,1 + 4 = 556

注意 i=0,1,3i = 0, 1, 3 三个位置都触发了”丢弃”:

  • i=0i=0:前面是 0(等效于没有前缀),取 -2
  • i=1i=1pre = -2,是负数,丢弃前面的 [-2],从 1 重新开始。
  • i=3i=3pre = -2,是负数,丢弃前面的 [1, -3],从 4 重新开始。而 i=4i=4i=6i=6 连续保留,构建出最优解 [4,1,2,1][4, -1, 2, 1](和为 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。这样在 O(1)O(1) 额外空间内就能追踪子数组边界。

乘积最大子数组#

将加法换成乘法后,负数的存在让问题变得更有趣——两个负数相乘会变成正数,所以当前的负数可能是未来的宝藏。这个变体需要同时维护”以 ii 结尾的最大乘积”和”以 ii 结尾的最小乘积”两个状态。详见 乘积最大子数组题解

环形子数组的最大和#

如果数组是环形的(首尾相连),最大子数组可能跨越数组两端。诀窍在于:环形最大 = max(普通最大, 总和 - 普通最小)。因为跨越两端的子数组,等价于”挖掉中间一段最小的”。详见 环形子数组的最大和

线段树维护区间最大子段和#

当数组需要频繁修改,并且每次修改后要快速回答”整个数组的最大子段和是多少”时,Kadane 的 O(n)O(n) 单次查询就不够用了。这时需要线段树来维护区间信息——而线段树节点的合并操作,正是 Kadane 算法分治视角的推广。

总结#

Kadane 算法虽然只有短短几行代码,但它身上凝聚了几个非常重要的算法设计思想:

  1. 强制结尾的 DP 状态定义dp[i]=以 i 结尾的最优解dp[i] = \text{以 i 结尾的最优解},这是子数组类 DP 的万能钥匙。
  2. 空间优化的来源识别dp[i]dp[i] 只依赖 dp[i1]dp[i-1] → 滚动变量 → O(1)O(1) 空间。
  3. DP 退化为贪心:当状态转移中的一种选择被另一种严格支配时,DP 可以简化为贪心决策。
  4. 分治视角:跨越中点的最大和 = 左半后缀最大 + 右半前缀最大,这直接导向线段树的节点合并。

💡 推荐练习顺序:最大子数组和(本题)→ 乘积最大子数组 → 环形子数组的最大和 → 线段树区间最大子段和。这个序列将 Kadane 算法的思想逐步泛化,覆盖了 DP、贪心、分治、线段树四个领域。

掌握了 Kadane 算法,你掌握的不仅仅是一道题,而是一整套处理”子数组最值”问题的思维框架。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Kadane 算法详解
https://www.hygen.red/posts/dsa/knowledge/kadane/
作者
Hygen
发布于
2026-06-30
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录