LeetCode 第 53 题:最大子数组和(Maximum Subarray)
字数 2021 2025-10-25 17:17:03

好的,我们这次来详细讲解 LeetCode 第 53 题:最大子数组和(Maximum Subarray)


1. 题目描述

给你一个整数数组 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

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


2. 题目理解

我们要找的是 连续子数组(不能跳着选,必须连续一段),并且它的和最大。

注意:数组可能包含负数,所以不是简单地整个数组求和,因为负数可能会让和变小。


3. 思路演进

3.1 暴力法(不可取,但有助于理解)

我们可以枚举所有可能的连续子数组,计算它们的和,然后取最大值。

  • 子数组起点 i 从 0 到 n-1
  • 子数组终点 j 从 i 到 n-1
  • 对每个 (i, j) 计算 sum(nums[i..j])

复杂度:O(n³) 如果每次重新求和,优化一下前缀和可以到 O(n²),但 n 很大时仍然太慢。


3.2 动态规划(Kadane 算法)思路 O(n)

核心思想是:以每个位置为结尾的最大子数组和,可以由前一个位置为结尾的最大子数组和推导出来

定义:

  • dp[i] 表示以第 i 个元素结尾的 最大子数组和

思考:

  • 如果已知 dp[i-1](以 i-1 结尾的最大和),那么对于位置 i
    • 我可以只取 nums[i] 自己(如果前面的 dp[i-1] 是负数,加上它只会让和变小,不如重新开始)
    • 或者我可以接上前面以 i-1 结尾的子数组(如果 dp[i-1] 是正数,加上它对增大和有帮助)

所以递推公式:

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

最后答案:

\[\max_{0 \le i < n} dp[i] \]


3.3 逐步推导示例

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] 为例:

i nums[i] dp[i] (以 i 结尾的最大和) 解释
0 -2 -2 只有自己
1 1 max(1, -2+1) = 1 前面是 -2,不如重新从 1 开始
2 -3 max(-3, 1 + (-3)) = -2 接上前面的 1 得到 -2,比单独 -3 大
3 4 max(4, -2 + 4) = 4 前面是 -2,加上反而小,不如从 4 开始
4 -1 max(-1, 4 + (-1)) = 3 接上 4 得 3
5 2 max(2, 3 + 2) = 5 接上 3 得 5
6 1 max(1, 5 + 1) = 6 接上 5 得 6(这是全局最大)
7 -5 max(-5, 6 + (-5)) = 1 接上 6 得 1
8 4 max(4, 1 + 4) = 5 接上 1 得 5

所以最大子数组和 = max(-2, 1, -2, 4, 3, 5, 6, 1, 5) = 6


3.4 空间优化

因为 dp[i] 只依赖于 dp[i-1],所以只需一个变量 current_sum 记录当前以 i 结尾的最大和,一个变量 max_sum 记录全局最大和。


4. 代码实现(Kadane 算法)

def maxSubArray(nums):
    current_sum = nums[0]
    max_sum = nums[0]
    
    for i in range(1, len(nums)):
        # 如果 current_sum 是负数,则舍弃前面的,从当前元素重新开始
        current_sum = max(nums[i], current_sum + nums[i])
        # 更新最大和
        max_sum = max(max_sum, current_sum)
    
    return max_sum

5. 分治法思路(进阶)

题目要求尝试分治法,我们来看看如何分治:

将数组从中间分成左右两半,最大子数组和可能出现在:

  1. 完全在左半部分
  2. 完全在右半部分
  3. 跨越中间(包含中间元素向左右延伸)

递归求解:

  • 左半部分最大和
  • 右半部分最大和
  • 跨越中点的最大和 = 中点向左延伸的最大和 + 中点向右延伸的最大和(不重复加中点)

最后取三者最大值。

复杂度:O(n log n),虽然比 Kadane 算法慢,但体现了分治思想。


分治法代码(供参考)

def maxSubArray(nums):
    def divide_conquer(left, right):
        if left == right:
            return nums[left]
        
        mid = (left + right) // 2
        
        # 左半部分最大和
        left_max = divide_conquer(left, mid)
        # 右半部分最大和
        right_max = divide_conquer(mid + 1, right)
        
        # 跨越中点的最大和
        # 从中点向左延伸的最大和
        left_cross_max = nums[mid]
        left_cross_sum = 0
        for i in range(mid, left - 1, -1):
            left_cross_sum += nums[i]
            left_cross_max = max(left_cross_max, left_cross_sum)
        
        # 从中点向右延伸的最大和
        right_cross_max = nums[mid + 1]
        right_cross_sum = 0
        for i in range(mid + 1, right + 1):
            right_cross_sum += nums[i]
            right_cross_max = max(right_cross_max, right_cross_sum)
        
        cross_max = left_cross_max + right_cross_max
        
        return max(left_max, right_max, cross_max)
    
    return divide_conquer(0, len(nums) - 1)

6. 总结

  • Kadane 算法(动态规划)是本题最优解,时间复杂度 O(n),空间 O(1)。
  • 关键思路:dp[i] 表示以 i 结尾的最大子数组和,递推时判断是否舍弃前面的和。
  • 分治法虽然复杂度高,但是练习分治思想的经典例题。

这样讲解清楚吗?我们可以再找一个题目继续。

好的,我们这次来详细讲解 LeetCode 第 53 题:最大子数组和(Maximum Subarray) 。 1. 题目描述 给你一个整数数组 nums ,请你找出一个具有 最大和 的连续子数组(子数组最少包含一个元素),返回其最大和。 示例 1: 示例 2: 示例 3: 进阶 :如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。 2. 题目理解 我们要找的是 连续子数组 (不能跳着选,必须连续一段),并且它的和最大。 注意:数组可能包含负数,所以不是简单地整个数组求和,因为负数可能会让和变小。 3. 思路演进 3.1 暴力法(不可取,但有助于理解) 我们可以枚举所有可能的连续子数组,计算它们的和,然后取最大值。 子数组起点 i 从 0 到 n-1 子数组终点 j 从 i 到 n-1 对每个 (i, j) 计算 sum(nums[i..j]) 复杂度:O(n³) 如果每次重新求和,优化一下前缀和可以到 O(n²),但 n 很大时仍然太慢。 3.2 动态规划(Kadane 算法)思路 O(n) 核心思想是: 以每个位置为结尾的最大子数组和,可以由前一个位置为结尾的最大子数组和推导出来 。 定义: dp[i] 表示以第 i 个元素结尾的 最大子数组和 。 思考: 如果已知 dp[i-1] (以 i-1 结尾的最大和),那么对于位置 i : 我可以只取 nums[i] 自己(如果前面的 dp[i-1] 是负数,加上它只会让和变小,不如重新开始) 或者我可以接上前面以 i-1 结尾的子数组(如果 dp[i-1] 是正数,加上它对增大和有帮助) 所以递推公式: \[ dp[ i] = \max(nums[ i], \ dp[ i-1] + nums[ i ]) \] 最后答案: \[ \max_ {0 \le i < n} dp[ i ] \] 3.3 逐步推导示例 以 nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] 为例: | i | nums[ i] | dp[ i ] (以 i 结尾的最大和) | 解释 | |---|--------|--------------------------|------| | 0 | -2 | -2 | 只有自己 | | 1 | 1 | max(1, -2+1) = 1 | 前面是 -2,不如重新从 1 开始 | | 2 | -3 | max(-3, 1 + (-3)) = -2 | 接上前面的 1 得到 -2,比单独 -3 大 | | 3 | 4 | max(4, -2 + 4) = 4 | 前面是 -2,加上反而小,不如从 4 开始 | | 4 | -1 | max(-1, 4 + (-1)) = 3 | 接上 4 得 3 | | 5 | 2 | max(2, 3 + 2) = 5 | 接上 3 得 5 | | 6 | 1 | max(1, 5 + 1) = 6 | 接上 5 得 6(这是全局最大) | | 7 | -5 | max(-5, 6 + (-5)) = 1 | 接上 6 得 1 | | 8 | 4 | max(4, 1 + 4) = 5 | 接上 1 得 5 | 所以最大子数组和 = max(-2, 1, -2, 4, 3, 5, 6, 1, 5) = 6 。 3.4 空间优化 因为 dp[i] 只依赖于 dp[i-1] ,所以只需一个变量 current_sum 记录当前以 i 结尾的最大和,一个变量 max_sum 记录全局最大和。 4. 代码实现(Kadane 算法) 5. 分治法思路(进阶) 题目要求尝试分治法,我们来看看如何分治: 将数组从中间分成左右两半,最大子数组和可能出现在: 完全在左半部分 完全在右半部分 跨越中间(包含中间元素向左右延伸) 递归求解: 左半部分最大和 右半部分最大和 跨越中点的最大和 = 中点向左延伸的最大和 + 中点向右延伸的最大和(不重复加中点) 最后取三者最大值。 复杂度:O(n log n),虽然比 Kadane 算法慢,但体现了分治思想。 分治法代码(供参考) 6. 总结 Kadane 算法 (动态规划)是本题最优解,时间复杂度 O(n),空间 O(1)。 关键思路: dp[i] 表示以 i 结尾的最大子数组和,递推时判断是否舍弃前面的和。 分治法虽然复杂度高,但是练习分治思想的经典例题。 这样讲解清楚吗?我们可以再找一个题目继续。