53. 最大子数组和 (Medium)

专题归类: 数组 · 动态规划 LeetCode 链接: https://leetcode.cn/problems/maximum-subarray/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

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

示例 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

补充说明:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 子数组最少包含一个元素,是连续的

题目详细分析

数据范围分析:

  • 数组长度最大 10^5,O(n^2) 不可行,需要 O(n) 或 O(n log n) 的算法。
  • 元素值范围 -10^4 ~ 10

输入输出特征:

  • 输入是整数数组,输出是一个整数(最大子数组和)。
  • 子数组必须连续,不是子序列(子序列可以跳过元素)。
  • 至少包含一个元素,所以答案不可能是空数组。

边界条件:

  • 数组只有一个元素 → 答案就是该元素本身。
  • 数组全部为负数 → 答案是最大的那个负数(不能选空数组)。
  • 数组全部为正数 → 答案是整个数组的和。
  • 最大子数组可能跨越数组的多个部分,包含正数和负数。

隐藏条件:

  • 负数的存在是解题的关键。负数会降低和,所以当累加和变成负数时,它对后续元素只有拖累作用,应该丢弃重来。
  • 问题等价于:在遍历过程中,决定每个元素是加入之前的子数组还是重新开始。

小白版直白理解

想象你在玩一个”捡金币”游戏。路上有一排格子,每个格子里有金币(正数)或陷阱(负数)。你必须连续走一段路,不能跳着走。

你的目标是:选一段连续的路,让你捡到的金币总和最大

直觉:

  • 遇到金币肯定要捡,因为赚了。
  • 遇到陷阱就要纠结了——如果之前捡了很多金币,这点陷阱能扛过去,后面可能还有更多金币;如果之前就捡得少,这个陷阱会让你血亏,不如从这个陷阱后面重新开始

其实这就是 Kadane 算法的核心思想:每一步都在想”是接着走还是重新开始”。

生活中的类似场景:

  • 股票交易中的最大收益区间
  • 连续几天的最高温度上升幅度
  • 比赛中状态最好的连续场次

解题思路

思路一:Kadane 算法 / 动态规划(推荐)

核心洞察:

定义 dp[i]nums[i] 结尾的最大子数组和。那么对于 nums[i] 有两种选择:

  1. 把它接在 nums[i-1] 的后面:dp[i] = dp[i-1] + nums[i]
  2. 以它为新起点重新开始:dp[i] = nums[i]

取两者中较大的:dp[i] = max(nums[i], dp[i-1] + nums[i])

最终答案是所有 dp[i] 中的最大值。

空间优化: 因为 dp[i] 只依赖 dp[i-1],所以只需要一个变量 cur_sum 来滚动更新。

def maxSubArray(nums):
    # cur_sum: 以当前位置结尾的最大子数组和
    # max_sum: 全局最大子数组和
    cur_sum = max_sum = nums[0]
    
    for num in nums[1:]:
        # 要么接上前面的,要么重新开始
        cur_sum = max(num, cur_sum + num)
        # 更新全局最大值
        max_sum = max(max_sum, cur_sum)
    
    return max_sum

复杂度: O(n) 时间,O(1) 空间。

思路二:贪心法(同 Kadane 的另一种理解)

遍历数组,如果当前累加和 cur_sum < 0,说明它对后续没有正面贡献,直接丢弃,从下一个元素重新累加。否则继续累加。

def maxSubArray_greedy(nums):
    cur_sum = 0
    max_sum = nums[0]
    
    for num in nums:
        cur_sum += num
        max_sum = max(max_sum, cur_sum)
        # 核心:如果累加和变负,丢弃重来
        if cur_sum < 0:
            cur_sum = 0
    
    return max_sum

关键区别: 贪心法在 cur_sum < 0 时重置为 0(相当于丢弃),而 DP 法用 max() 选择是否重新开始。两者本质相同。

思路三:分治法(进阶)

将数组分为左右两半,最大子数组和来自三种情况:

  1. 完全在左半部分
  2. 完全在右半部分
  3. 跨越中点(从中点向左扩展的最大和 + 从中点向右扩展的最大和)

取三者最大值。时间复杂度 O(n log n),空间 O(log n)。

def maxSubArray_divide(nums):
    def divide_and_conquer(l, r):
        if l > r:
            return -float('inf')
        if l == r:
            return nums[l]
        
        mid = (l + r) // 2
        
        # 跨越中点的最大和
        left_max = cur = 0
        for i in range(mid - 1, l - 1, -1):
            cur += nums[i]
            left_max = max(left_max, cur)
        
        right_max = cur = 0
        for i in range(mid + 1, r + 1):
            cur += nums[i]
            right_max = max(right_max, cur)
        
        cross_max = nums[mid] + left_max + right_max
        
        # 取三者最大值
        return max(divide_and_conquer(l, mid - 1),
                   divide_and_conquer(mid + 1, r),
                   cross_max)
    
    return divide_and_conquer(0, len(nums) - 1)

易错点

  • 初始化问题max_sum 必须初始化为 nums[0] 而不是 0。如果全为负数但初始化为 0 会返回错误结果 0。
  • 空数组返回:题目说至少一个元素,但如果是空数组需要特殊处理。
  • 重置时机:贪心法中重置 cur_sum = 0 而不是重置为当前元素值——因为下一轮循环会先加上当前元素。
  • 负数处理:当累加和加上一个负数后虽然变小了但不一定需要重置(后面可能有更大的正数)。
  • 连续要求:记住是连续子数组,不是子序列。如果用排序就错了。

框架提炼

Kadane 算法通用模板(最大子数组和系列):

def maxSubarrayKadane(nums):
    # Step 1: 初始化
    cur = nums[0]    # 以当前位置结尾的子数组最大和
    best = nums[0]   # 全局最大和
    
    # Step 2: 遍历
    for num in nums[1:]:
        # 状态转移:要么接上,要么重开
        cur = max(num, cur + num)
        # 更新全局最优
        best = max(best, cur)
    
    return best

Kadane 变体适用场景:


关联题目

  • 152-乘积最大子数组 — Kadane 算法的变体,区别在于乘积需要考虑负负得正,所以需要同时维护最大值和最小值。
  • 918-环形子数组的最大和 — 在 Kadane 基础上扩展到环形数组,需要同时计算最大子数组和与最小子数组和。
  • 560-和为 K 的子数组 — 同样是子数组问题,但用的是前缀和+哈希表而非 Kadane,因为目标和是定值 k 而非最值。
  • 121-买卖股票的最佳时机 — 本质上也是找最大差值(相当于找 prices[i] - min_price 的最大值),可以用 Kadane 思路理解。