Skip to content

Latest commit

 

History

History
193 lines (136 loc) · 8.54 KB

File metadata and controls

193 lines (136 loc) · 8.54 KB

LeetCode 53. 最大子数组和 (Maximum Subarray)

难度:Medium | 标签:数组、动态规划、贪心、分治 | DP 入门必刷 ⭐


一、题目

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

约束

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

示例

输入 输出 子数组
[-2,1,-3,4,-1,2,1,-5,4] 6 [4,-1,2,1]
[1] 1 [1]
[5,4,-1,7,8] 23 全数组

题目链接:https://leetcode.cn/problems/maximum-subarray/


二、解题思路(学习重点)

1. Kadane 算法:DP 的最经典入门

从暴力到 DP 的思路

  • 暴力枚举所有子数组 O(n³),加上前缀和优化到 O(n²),但仍未抹去重复计算。
  • 关键观察:枚举以 i 结尾的最大子数组时,“以 i-1 结尾的最大子数组”不是负的话就该接上,负的话从 i 重起。于是状态只依赖前一个,这就是 DP

定义 f(i) = 以 nums[i] 结尾 的最大子数组和

转移: $$f(i) = \max(f(i-1) + nums[i],; nums[i])$$

含义:要么 接着前面的扩展,要么 从 i 重新开始(前面的负贡献就丢掉)。

最终答案:max(f(0), f(1), ..., f(n-1))

学习点 ①:DP 状态定义要"以 i 结尾"而非"前 i 个的最大",才能保证连续性。这是一类 DP 题共同的套路(同样适用于 LIS、乘积最大子数组等)。

2. 空间优化到 O(1)

f(i) 只依赖 f(i-1),用一个变量滚动即可。

3. 贪心理解(同一段代码的另一种说法)

维护当前累加和 cur:若 cur < 0,对未来一定是负贡献,清零重启

4. 容易踩的坑

处理
全负数组(如 [-3,-1,-2])误返回 0 答案初始化为 Integer.MIN_VALUEnums[0]不能 是 0
用滑动窗口正负判断 元素含负数时窗口不单调,滑动窗口失效,必须 DP

三、Java 题解

解法 A:Kadane O(1) 空间(推荐)

class Solution {
    public int maxSubArray(int[] nums) {
        int cur = nums[0], best = nums[0];
        for (int i = 1; i < nums.length; i++) {
            cur = Math.max(cur + nums[i], nums[i]); // 接 or 重启
            best = Math.max(best, cur);
        }
        return best;
    }
}

记忆口诀

"接着走,还是重开?取大者;全程最大记 best。"

解法 B:分治(O(n log n),面试加分)

把数组对半分,最大子数组要么在左、要么在右、要么 横跨中点。横跨情况从中点向两边各做一次贪心扩展。

class Solution {
    public int maxSubArray(int[] nums) {
        return divide(nums, 0, nums.length - 1);
    }
    private int divide(int[] a, int l, int r) {
        if (l == r) return a[l];
        int m = (l + r) >>> 1;
        int leftMax = divide(a, l, m);
        int rightMax = divide(a, m + 1, r);

        // 横跨中点:从 m 向左、从 m+1 向右各取最大前缀
        int sum = 0, lMax = Integer.MIN_VALUE;
        for (int i = m; i >= l; i--) { sum += a[i]; lMax = Math.max(lMax, sum); }
        sum = 0; int rMax = Integer.MIN_VALUE;
        for (int i = m + 1; i <= r; i++) { sum += a[i]; rMax = Math.max(rMax, sum); }

        return Math.max(Math.max(leftMax, rightMax), lMax + rMax);
    }
}

分治版是 「线段树」 解决区间最大子段和的雏形(支持单点修改 + 区间查询)。


四、复杂度

解法 时间 空间
Kadane O(n) O(1)
分治 O(n log n) O(log n) 递归栈

五、示例验证

nums = [-2,1,-3,4,-1,2,1,-5,4]

i nums[i] cur (接 vs 重开) best
0 -2 -2 -2
1 1 max(-2+1, 1)=1 1
2 -3 max(1-3, -3)=-2 1
3 4 max(-2+4, 4)=4 4
4 -1 max(4-1, -1)=3 4
5 2 max(3+2, 2)=5 5
6 1 max(5+1, 1)=6 6
7 -5 max(6-5, -5)=1 6
8 4 max(1+4, 4)=5 6

输出 6


六、复盘与延伸

一句话总结

以 i 结尾的最大子数组:要么把 i 接上前面,要么从 i 重新开始。

新手常见疑问(FAQ)

Q1:为什么状态是"以 i 结尾",而不是"前 i 个的最大子数组和"? A:“以 i 结尾”才能表达连续性:f(i-1) 的子数组以 i-1 结尾,接上 nums[i] 则仍为连续。若定义为“前 i 个的最大子数组和”,那个子数组可能末尾不是 i-1,加上 nums[i] 就不连续了。

Q2:全负数组返回 0 对不对? A:不对。题目要求子数组至少一个元素,如 [-3,-1,-2] 应返 -1。初始化 best = nums[0](不能是 0),算法天然正确。

Q3:为什么不能用滑动窗口(双指针)? A:滑动窗口需要“扩展一定使之变大、收缩一定变小”的单调性。含负数时扩展可能让和变小,收缩可能让和变大,没有单调性,滑窗失效。这是题目“含负数”与“全正数”的本质区别。

Q4:为什么叫 Kadane? A:Jay Kadane 上世纪 80 年代提出该算法,是“DP 滑动状态”的原型。面试可以报出名字加分,但不要独压这条。

Q5:怎么同时返回最大子数组的起止下标? A:重启时(cur = nums[i])记 tmpStart = i;更新 best 时记 [tmpStart, i]

Q6:分治版里左半为什么写 divide(l, mid) 而不是 divide(l, mid - 1) A:区间是闭区间 [l, r],划分必须不重不漏地覆盖整个区间。

  • 左半 [l, mid] + 右半 [mid+1, r] = [l, r]
  • 若写成 [l, mid-1] + [mid+1, r]nums[mid] 就被两边都丢掉,结果错;而且当 l == r-1mid = ldevide(l, mid-1) 变成 devide(l, l-1),base case l == r 也命中不了,要么死循环要么爆栈。

这和二分查找里 r = mid - 1 不一样:二分时已经"测过 nums[mid] 不是目标",所以排除;分治里 mid 还要参与左半递归和跨中点求和,不能排除

跨中点求和 lmax + rmax - nums[mid] 也是建立在"mid 同属左半"的前提上——左右两段都把 nums[mid] 算进去了,所以减一次。

面试官常见 follow-up

  1. "改成求最小子数组和呢?" → 同样模板,交换 max/min 即可;或求总和 − 最大子数组和。
  2. "求乘积最大子数组呢?" → 负数 × 负数 = 正,要 同时维护 max 与 min,遇到负数交换。即 LC 152
  3. "环形数组的最大子数组和呢?" → 两种情况取最大:a) 不跨环→ Kadane;b) 跨环→ total - 最小子数组和。注意全负数要特判。即 LC 918
  4. "允许删除一个元素后的最大子数组和?" → 双状态 DP:f[i][0] 不删、f[i][1] 已删。即 LC 1186
  5. "返回所有子数组中第 k 大的和呢?" → 前缀和 + 堆;或排序后二分。实际面试少见但可能被追问。
  6. "数据流场景(来一个处理一个)?" → Kadane 天然在线:cur = max(cur+x, x); best = max(best, cur);

自我提问

  1. 为什么状态要"以 i 结尾"?→ 这样保证子数组连续;如果定义"前 i 个的最大",无法表达"不取 i" vs "取 i" 的连续性。
  2. 全负数组怎么办?→ 初始化 best = nums[0](不能是 0),算法天然正确。
  3. 数据流场景?→ Kadane 天然支持,每来一个 xcur = max(cur+x, x); best = max(best, cur);
  4. 如何返回最大子数组的 下标区间 ?→ 重启时记录 start = i,更新 best 时记录 [start, i]

同类型推荐(最大子段和家族)