LeetCode 53. 最大子数组和

一、题目与初始思路误区

题目要求

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

  • 子数组是连续的,子序列可以不连续,注意区分
  • 数组中包含正数、负数、0
  • 全负数数组返回最大的那个负数

一开始的双指针思路为什么行不通?

我最开始的直觉是用左右指针维护子数组区间、边走边更新和,也就是滑动窗口思路,但这个思路在这道题里是走不通的。

核心原因:滑动窗口的前提是「单调性」------ 右指针右移时区间和有固定的增减趋势,左指针右移时也有固定的增减趋势。

  • 对于全正数数组:右移右指针和一定变大,右移左指针和一定变小,双指针成立
  • 对于有正有负的数组:右指针往右走,遇到负数和会变小;左指针往右走,去掉负数和反而会变大
  • 没有固定的单调性,就没法总结出指针移动的统一规则,滑动窗口自然就不适用

这也是这道题的核心考点:不是所有子数组问题都能用滑动窗口,有正有负的求和问题优先考虑动态规划


二、思路推导:Kadane 算法

动态规划思路

定义状态:dp[i] 表示以第 i 个元素结尾的最大子数组和。 对于每个元素,有两种选择:

  1. 接上前面的子数组:和为 dp[i-1] + nums[i]

  2. 自己重新开一个子数组:和为 nums[i]

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

最终答案就是整个 dp 数组中的最大值。

  • 时间复杂度:O (n)
  • 空间复杂度:O (n)

3. Kadane 算法:空间优化版

观察状态转移方程可以发现,计算 dp[i] 只需要用到 dp[i-1] 这一个前序状态,不需要保存整个 dp 数组。 只用一个变量 preSum 维护前一个位置的状态,就能完成全部计算,空间复杂度直接优化到 O (1)。

核心思想一句话概括:遍历每个元素时,只判断一件事 ------ 前面的子数组有没有正向收益。有就带上,没有就从当前元素重新开始。

三、复盘

坑 1:maxSum 初始化为 0 → 全负数场景直接报错

错误代码片段
复制代码
int maxSum = 0;
现象

输入全负数数组 [-3,-1,-2],正确答案是 -1,但代码返回 0。

原因

题目要求子数组至少包含一个元素,不能为空。初始化为 0 相当于默认 "空数组和为 0" 是合法解,违反了题目要求。

修正

maxSum 必须初始化为 nums[0],天然兼容全负数场景 ------ 哪怕全是负数,最大的那个负数也会被正确记录。

坑 2:preSum 初始为 nums 0 + 从 i=0 开始遍历 → 首元素重复计算

错误代码片段
复制代码
int preSum = nums[0];
for (int i = 0; i < nums.length; i++) {
    preSum += nums[i];
    // ...
}
现象

输入单元素数组 [2],正确答案是 2,但代码返回 4。

原因

初始化时已经把第一个元素算进了 preSum,循环又从第 0 个元素开始,第一个元素被累加了两遍,结果必然翻倍。

修正规则

初始化占了第一个元素,循环就从第二个元素开始;初始化没占,循环就从第一个元素开始。两边不能重复。

  • 方案 A:preSum = nums[0] + for(i=1)
  • 方案 B:preSum = 0 + 从头遍历所有元素

两种正确初始化方式对比

方案 preSum 初始值 遍历起始 特点
A nums0 i=1(第二个元素) 下标逻辑清晰,对应动态规划 dp 数组思路
B 0 从第一个元素开始 增强 for 循环不用管下标,手写不易错

四、最终

最终选择「preSum 初始为 0 + 增强 for 循环全量遍历」的方案,原因有三个:

  1. 避开下标坑:不用纠结 i 从 0 还是 1 开始,彻底杜绝首元素重复计算的错误
  2. 逻辑贴合直觉:完全对应「正收益就累加、负收益就重开」的朴素思路,不用死记状态转移方程
  3. 代码极简:增强 for 循环代码更短,手写速度快,出错概率低

完整可 AC 代码

java 复制代码
class Solution {
    public int maxSubArray(int[] nums) {
        // 全局最大子数组和:必须初始为第一个元素,兼容全负数场景
        int maxSum = nums[0];
        // 当前累计和:初始为0作为空前缀,第一个元素会自动重置
        int preSum = 0;

        // 增强for遍历所有元素,无需处理下标
        for (int num : nums) {
            if (preSum > 0) {
                // 前面累计和为正,有正向收益,接上当前元素
                preSum += num;
            } else {
                // 前面累计和≤0,是累赘,从当前元素重新开始
                preSum = num;
            }
            // 每一步都更新全局最大值
            maxSum = Math.max(maxSum, preSum);
        }
        return maxSum;
    }
}

五、逐行拆解 + 样例模拟

两个变量的职责分工

  • preSum:临时变量,表示以当前元素结尾的子数组的最大和,只关心当前段的累计收益
  • maxSum:全局变量,记录遍历过程中出现过的所有子数组和的最大值,是最终答案

标准样例完整模拟

输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 正确答案:6(对应子数组 [4,-1,2,1]

一步步执行:

  1. 初始:maxSum = -2preSum = 0
  2. num=-2:preSum≤0 → preSum=-2;maxSum = max(-2,-2) = -2
  3. num=1:preSum=-2≤0 → preSum=1;maxSum = max(-2,1) = 1
  4. num=-3:preSum=1>0 → preSum=1+(-3)=-2;maxSum = max(1,-2) = 1
  5. num=4:preSum=-2≤0 → preSum=4;maxSum = max(1,4) = 4
  6. num=-1:preSum=4>0 → preSum=4+(-1)=3;maxSum = max(4,3) = 4
  7. num=2:preSum=3>0 → preSum=3+2=5;maxSum = max(4,5) = 5
  8. num=1:preSum=5>0 → preSum=5+1=6;maxSum = max(5,6) = 6
  9. num=-5:preSum=6>0 → preSum=6+(-5)=1;maxSum = max(6,1) = 6
  10. num=4:preSum=1>0 → preSum=1+4=5;maxSum = max(6,5) = 6

最终返回 6,与标准答案完全一致。

边界用例验证:全负数

输入:nums = [-3,-1,-2]

  1. 初始:maxSum = -3preSum = 0
  2. num=-3:preSum≤0 → preSum=-3;maxSum=-3
  3. num=-1:preSum=-3≤0 → preSum=-1;maxSum=-1
  4. num=-2:preSum=-1≤0 → preSum=-2;maxSum=-1

返回 -1,完全正确。


六、面试相关

口述思路

这道题我用 Kadane 算法来做,本质是动态规划的空间优化版本。定义 preSum 表示以当前元素结尾的最大子数组和,对于每个元素,如果前面的累计和大于 0,说明有正向收益,就接上前面的子数组;如果累计和小于等于 0,说明前面是累赘,就从当前元素重新开始。

因为每一步只需要前一个状态,所以不需要完整的 dp 数组,只用一个变量维护即可。遍历过程中同时维护全局最大值,最终返回。时间复杂度 O (n),空间复杂度 O (1)。

高频追问

  1. 时间复杂度和空间复杂度分别是多少? 时间 O (n),只遍历一次数组;空间 O (1),只用了两个变量。

  2. 分治法怎么做? 把数组分成左右两半,最大子数组和要么在左半段,要么在右半段,要么跨越中点。分别计算三种情况取最大值,时间复杂度 O (nlogn),实际运行不如 Kadane 算法高效,了解思路即可。

  3. 如果要求返回最大子数组的起止下标怎么办? 在更新 preSum 重置的时候记录左边界,更新 maxSum 的时候记录左右边界即可,核心逻辑不变,只需要额外加几个变量记录位置。

  4. 为什么不能用滑动窗口? 因为数组中有正有负,区间和不具备单调性 ------ 右指针右移和不一定变大,左指针右移和不一定变小,没有固定的指针移动规则,所以滑动窗口不适用。


七、复习速记

累计为正就累加,累计为负就重开; 全局最值首值起,全负场景不翻车。

总结

这道题是动态规划的入门经典,也是「思路简单、细节坑多」的典型代表。核心算法逻辑一句话就能讲完,但新手手写非常容易在初始化、下标这些细节上翻车。

复习优先级:

  1. 先记核心思想:正收益累加、负收益重开
  2. 再记两个红线:maxSum 绝对不能初始为 0、初始化和遍历不能重复覆盖首元素
  3. 最后选最顺手的写法背熟:增强 for 循环版本,天然避开下标坑
相关推荐
江屿风1 小时前
【科普】【差集&交集概念落地云相册】流食般投喂
开发语言·c++·笔记·算法·云相册
数模竞赛Paid answer1 小时前
2023年五一杯数学建模A题无人机定点投放问题求解全过程完整文档及程序
算法·数学建模·无人机·五一杯
科学实验家1 小时前
dp子序列问题,好难呀
算法
wabs66610 小时前
关于图论【卡码网117.软件构建的思考】
数据结构·算法·软件构建·图论·卡码网
mifengxing11 小时前
LeetCode 41.缺失的第一个正数|Hard题O(n)+O(1)最优解法深度解析
java·算法·leetcode·排序算法
Tongzhi202611 小时前
从部署到运维:通芝科技无感考勤一体机的全流程效率解析
运维·数据结构·科技·算法·贪心算法
Wang's Blog11 小时前
AI Agent白手起家30: 动态示例选择器之根据长度选择 Few Shot 示例
算法
oyguyteggytrrwwwrt13 小时前
自制交叉线路识别算法
算法
手写码匠13 小时前
华为云Flexus+DeepSeek征文|Dify 多智能体协同编排实战:R1 规划 + V3 执行,构建企业 Agent 团队
人工智能·深度学习·算法·aigc