mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
2182 字
6 分钟
最大子数组和
2026-06-30

题目描述#

题目链接

给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

输入#

整数数组 nums

输出#

最大子数组和

约束条件#

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

进阶约束#

如果你已经实现复杂度为 O(n)O(n) 的解法,尝试使用更为精妙的分治法求解。

示例1#

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6

解释:连续子数组 [4,-1,2,1] 的和最大,为 6

示例2#

输入:nums = [1]
输出:1

示例3#

输入:nums = [5,4,-1,7,8]
输出:23

解题思路#

这是一道经典的最大子数组和问题,也是 Kadane 算法的代表性题目。数组中的元素可能为负数,因此不能简单地从第一个正数开始一直累加——负数的出现会让累加和”缩水”,但有时为了连接后面的正数段,我们又必须”忍耐”中间的负数。

💡 一句话概括:在遍历数组时,我们始终面临一个选择——是延续前面的累加,还是重新开始。如果前面的累加是正的,保留它;如果是负的,丢弃它。

🔗 延伸阅读:Kadane 算法的完整原理推导(DP 视角 + 贪心视角)、变体扩展和线段树推广,详见 Kadane 算法详解(知识点专题文章)。

核心问题可以归结为:当遍历到某个位置时,我该延续前面的子数组,还是从这里重新开始?

从暴力枚举出发#

最直接的做法是枚举所有子数组并计算和:

class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n = len(nums)
ans = float('-inf')
for i in range(n):
cur = 0
for j in range(i, n):
cur += nums[j]
ans = max(ans, cur)
return ans

这段代码枚举了所有可能的起点 i 和终点 j,在 O(n2)O(n^2) 时间内计算出最大子数组和。

瓶颈分析:当 n105n \le 10^5 时,O(n2)=1010O(n^2) = 10^{10} 次操作,远超时间限制。而且内部循环做了大量重复计算——以子数组 [1,3][2,3] 为例,它们的求和区间高度重叠,但暴力法每次都是从 i 重新累加,完全没有复用前一次的信息。

优化方向:能否只遍历一次数组,在遍历过程中动态地做决策?

动态规划:Kadane 算法#

状态定义#

定义 dp[i]dp[i]nums[i]\text{nums}[i] 结尾的最大子数组和。注意这里的关键限制是”必须以 nums[i]\text{nums}[i] 结尾”——这让我们可以从子问题递推。

💡 DP 设计技巧:当问题问”任意区间的最优值”,常见的套路是强制状态以当前位置结尾,然后在所有 dp[i]dp[i] 中取最大值。这让我们获得了”前一个状态 + 当前元素”的递推关系。

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

  • 自成一派:从 nums[i]\text{nums}[i] 开始一个全新的子数组,和为 nums[i]\text{nums}[i]
  • 延续前朝:把 nums[i]\text{nums}[i] 接到以 nums[i1]\text{nums}[i-1] 结尾的最大子数组后面,和为 dp[i1]+nums[i]dp[i-1] + \text{nums}[i]

取两者中的较大值,得到状态转移方程:

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

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

空间优化#

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

同时注意到,dp[i1]dp[i-1] 只有在 > 0 时才对 dp[i]dp[i] 有正向贡献——如果 dp[i1]0dp[i-1] \le 0,那么 max(nums[i],dp[i1]+nums[i])=nums[i]\max(\text{nums}[i], dp[i-1] + \text{nums}[i]) = \text{nums}[i],等价于「丢弃之前的所有累加」。

因此代码可以简化为:

class Solution:
def maxSubArray(self, nums: List[int]) -> int:
ans = float('-inf')
pre = 0
for x in nums:
pre = max(pre, 0) + x
ans = max(ans, pre)
return ans

关键一行 pre = max(pre, 0) + x 的语义:

  • 如果 pre > 0,说明前面的累积对当前元素有正向贡献 → 累加上 x
  • 如果 pre \le 0,说明前面的累积只会拖累当前元素 → 丢弃,从 x 重新开始

🧠 直觉理解:想象你是一个股民,每天都有盈亏。pre 就是你当前的累计收益max(pre, 0) 就是在问自己——“我之前是赚了还是亏了?“亏了就清仓从头开始,赚了就继续持有。

这正是 Kadane 算法最优雅的写法——将 DP 状态转移与贪心决策融为一体。

执行过程追踪#

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] 为例,追踪 preans 的变化:

ix决策preans
0-∞
0-2pre ≤ 0,丢弃,0 + (-2)-2-2
11pre ≤ 0,丢弃,0 + 111
2-3pre > 0,保留,1 + (-3)-21
34pre ≤ 0,丢弃,0 + 444
4-1pre > 0,保留,4 + (-1)34
52pre > 0,保留,3 + 255
61pre > 0,保留,5 + 166
7-5pre > 0,保留,6 + (-5)16
84pre > 0,保留,1 + 456

最终 ans = 6,对应子数组 [4, -1, 2, 1]

可以观察到,pre 在位置 i=0, 1, 3≤ 0 时被”重置”——这恰好对应了”切断负数前缀,重新开始”的贪心决策。最终的最大值 6 出现在 i=6

复杂度#

  • 时间复杂度O(n)O(n),只需遍历数组一次。
  • 空间复杂度O(1)O(1),仅使用 preans 两个变量。

O(n)O(n) 已是最优——因为输出结果至少需要遍历一次数组才能确定。

解题思路(分治法)#

除了 DP,题目的进阶要求也提示我们尝试分治法。分治法的思路是将数组从中间分成左右两半,那么最大子数组有三种可能的位置:

情况描述求解方式
① 完全在左半最大子数组的左右端点均 ≤ mid递归左半
② 完全在右半最大子数组的左右端点均 > mid递归右半
③ 跨越中点左端点在左半,右端点在右半从中点向两侧扫描

前两种情况直接递归求解,第三种情况需要额外处理——从中点向左扫描求最大后缀和,从中点+1 向右扫描求最大前缀和,两者相加:

class Solution:
def maxSubArray(self, nums: List[int]) -> int:
def divide(l: int, r: int) -> int:
if l == r:
return nums[l]
mid = (l + r) // 2
# 递归求解左右
left_max = divide(l, mid)
right_max = divide(mid + 1, r)
# 跨越中点的最大和
cur = 0
left_cross = float('-inf')
for i in range(mid, l - 1, -1):
cur += nums[i]
left_cross = max(left_cross, cur)
cur = 0
right_cross = float('-inf')
for i in range(mid + 1, r + 1):
cur += nums[i]
right_cross = max(right_cross, cur)
cross_max = left_cross + right_cross
return max(left_max, right_max, cross_max)
return divide(0, len(nums) - 1)

💡 为什么学分治法? 虽然复杂度不如 Kadane,但分治法中”合并区间”的操作,正是线段树维护区间最大子段和的核心——当需要支持动态修改数组元素并快速查询任意区间答案时,分治 + 线段树是唯一选择。Kadane 做不到这一点。

两种方法的对比:

对比项Kadane(DP)分治法
时间复杂度O(n)O(n)O(nlogn)O(n \log n)
空间复杂度O(1)O(1)O(logn)O(\log n)
代码简洁度⭐⭐⭐⭐⭐⭐⭐⭐
思维难度需理解 DP 状态定义结构清晰,但跨越中点处理稍繁琐
扩展性仅适合静态最大子数组和可扩展为线段树维护区间信息

代码优化#

max(pre, 0) + xmax(x, pre + x) 的等价性#

# 写法 1:强调"丢弃负前缀"(更直观)
pre = max(pre, 0) + x
# 写法 2:强调"取或不取"(更贴近 DP 方程)
pre = max(x, pre + x)

写法 1 更直观地体现了贪心决策——「之前和为正则保留,为负则丢弃」。写法 2 更贴近标准 DP 状态转移方程。两种写法完全等价,按个人偏好选择。

初始化细节#

ans = float('-inf') # 正确处理全负数数组
pre = 0

⚠️ 注意ans 初始化为 float('-inf') 而非 0,确保全负数数组(如 nums = [-3, -2, -1])也能正确返回 -1。若用 0 则会错误地返回 0(空子数组在本题中是不允许的)。

等价的初始化方式:ans = nums[0](题目保证 nums.length >= 1),两种写法都是安全的。

扩展:返回子数组本身#

如果题目要求返回最大子数组而不仅是最大值,可以在维持 pre 的同时追踪子数组的左右端点:

class Solution:
def maxSubArray(self, nums: List[int]) -> List[int]:
ans = float('-inf')
pre = 0
start = end = 0
cur_start = 0
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]

💡 技巧:每次 pre <= 0 触发”丢弃”时,同步更新 cur_start 为当前位置;当 pre 刷新历史记录时,将 cur_start 封存到 startO(1)O(1) 额外空间即可追踪子数组边界。

总结#

「最大子数组和」是动态规划的入门经典,它的核心在于定义以每个位置结尾的最优状态,通过一个精妙的转移方程将问题降维到 O(n)O(n)

解决本题后,推荐按以下顺序挑战延伸题目——这条路径将 Kadane 的思想逐步泛化,覆盖 DP、贪心、分治、线段树四个领域:

  • 152. 乘积最大子数组:将加法换为乘法,负数会翻转符号——需要同时维护最大值和最小值
  • 918. 环形子数组的最大和:首尾相连,诀窍是”环形最大 = max(普通最大, 总和 - 普通最小)”
  • 线段树维护区间最大子段和:数组可修改时,如何用分治结构快速回答任意区间的最大子数组和?

🧠 核心收获:掌握了”以某位置结尾的最优状态”这一 DP 设计技巧,你就能从容应对一大类子数组问题。这不是一道题的技巧,而是一类题的思维框架。

分享

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

最大子数组和
https://www.hygen.red/posts/hot100/05-普通数组/013最大子数组和/
作者
Hygen
发布于
2026-06-30
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录