53. 最大子数组和

0 阅读7分钟

一、 题目与代码重现

题目描述

  • 题目名称:53. 最大子数组和

  • 问题描述:给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。

  • 输入输出示例

    • 示例 1:输入 nums = [-2,1,-3,4,-1,2,1,-5,4],输出 6(子数组 [4,-1,2,1]
    • 示例 2:输入 nums = [1],输出 1
    • 示例 3:输入 nums = [5,4,-1,7,8],输出 23
  • 限制条件

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

原始代码

javascript

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    let currentSum = nums[0];  // 以当前元素结尾的最大子数组和
    let maxSum = nums[0];      // 全局最大子数组和

    for (let i = 1; i < nums.length; i++) {
        // 要么从 nums[i] 重新开始,要么继续累加
        currentSum = Math.max(nums[i], currentSum + nums[i]);
        maxSum = Math.max(maxSum, currentSum);
    }
    
    return maxSum;
};

二、 标准解题逻辑步骤

本题的核心思想是 Kadane 算法,属于动态规划的一种简化形式。

核心思路梳理

局部最优 + 全局最优:遍历数组时,维护两个关键值——以当前元素结尾的最大子数组和全局最大子数组和

详细步骤解析

步骤操作说明逻辑依据
1. 初始化将 currentSum 和 maxSum 都设为 nums[0]因为数组非空,第一个元素本身就是一个有效的子数组
2. 遍历数组从索引 i = 1 开始遍历到末尾每一步都在做决策:是否将当前元素纳入之前的子数组
3. 状态转移currentSum = Math.max(nums[i], currentSum + nums[i])如果 nums[i] 比 currentSum + nums[i] 还大,说明前面累加的部分对当前元素是"拖累",不如从当前元素重新开始
4. 更新全局最优maxSum = Math.max(maxSum, currentSum)每一步都检查当前局部最优是否超越了之前的全局最优
5. 返回结果循环结束后返回 maxSum遍历完成时已保存全局最大子数组和

直观理解(用示例解释)

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

inums[i]currentSum(计算过程)maxSum(更新后)
0-2-2(初始化)-2
11max(1, -2+1=-1) = 1max(-2, 1) = 1
2-3max(-3, 1-3=-2) = -2max(1, -2) = 1
34max(4, -2+4=2) = 4max(1, 4) = 4
4-1max(-1, 4-1=3) = 3max(4, 3) = 4
52max(2, 3+2=5) = 5max(4, 5) = 5
61max(1, 5+1=6) = 6max(5, 6) = 6
7-5max(-5, 6-5=1) = 1max(6, 1) = 6
84max(4, 1+4=5) = 5max(6, 5) = 6

最终返回 6 ✅


三、 API 与语法深度解析

API/语法用途说明参数详解本题适用场景
Math.max(a, b)返回两个数值中的较大值ab 为数字类型用于状态转移和全局最优更新,简洁高效
nums.length获取数组长度无参数控制循环边界,时间复杂度与长度成正比
var maxSubArray = function(nums) { ... }函数表达式定义nums 为整数数组参数LeetCode 标准的函数定义方式
let 声明块级作用域变量可重新赋值用于存储动态变化的 currentSum 和 maxSum

关键语法点说明

  • Math.max(nums[i], currentSum + nums[i]) :这里巧妙地用 nums[i] 作为"重新开始"的选项。如果 currentSum 是负数,加上 nums[i] 会变得更小,此时 nums[i] 大于 currentSum + nums[i],相当于放弃了前面的累加,从当前位置重新开始子数组。
  • 初始化 nums[0] :由于题目保证 nums.length >= 1,直接取第一个元素是安全的。这避免了对空数组的特判。

四、 得分重点与理解难点划分

得分重点(考察核心)

考点说明
动态规划思想能否识别出"最优子结构"——即以每个位置结尾的最大子数组和依赖于前一个位置的状态
状态压缩能否将二维 DP 压缩为一维变量(只需 currentSum 和 maxSum
负数的正确处理遇到负数时不能简单地跳过,需要判断是否值得累加
一次遍历 O(n)是否能在单次遍历中完成所有计算

理解难点(思维盲区)

难点解析
为什么 currentSum + nums[i] 可能小于 nums[i]当 currentSum 为负数时,累加反而会拖累当前值,此时应该"丢弃"前面的子数组,从当前元素重新开始。许多初学者误以为必须包含前面的元素。
全局最大不一定在最后最大子数组可能出现在数组的中间位置,因此每一步都需要更新 maxSum,不能只依赖最后的 currentSum
全负数数组的处理即使所有元素都是负数,答案也是其中最大的那个负数。算法中 currentSum = max(nums[i], currentSum + nums[i]) 保证了这一点。

五、 极易踩错的细节与边界错误

边界条件清单

场景错误写法示例正确写法原因分析
数组长度为 1直接循环 for (let i=0; i<nums.length; i++) 导致重复计算从 i=1 开始遍历,或初始化 currentSum=0 从 i=0 开始避免重复累加第一个元素
全负数数组初始化 maxSum = 0,返回 0初始化 maxSum = nums[0]题目要求子数组至少包含一个元素,空数组和为 0 不合法
数值溢出风险使用 Number.MAX_SAFE_INTEGER 作为初始比较值直接用 nums[0] 初始化JS 中虽然不会溢出,但用 nums[0] 更简洁规范
空数组(虽题目保证非空)直接访问 nums[0] 报错添加 if (nums.length === 0) return 0虽然本题不涉及,但这是良好的防御性编程习惯

常见逻辑陷阱

  1. 双重循环暴力法for (let i=0; i<n; i++) for (let j=i; j<n; j++) 会产生 O(n²) 的复杂度,在 n=10^5 时会超时。
  2. 错误的状态转移currentSum = currentSum + nums[i] 而不做 max 比较,会导致负数累积拖垮结果。
  3. 忘记更新全局最优:只维护 currentSum 而不更新 maxSum,最后返回 currentSum 会丢失中间出现的更大值。

六、 复杂度对比与优化方案

基础写法对比

解法核心思路时间复杂度空间复杂度适用场景
暴力枚举枚举所有子数组起点和终点并求和O(n³) 或 O(n²)O(1)仅在 n ≤ 100 时可用
前缀和优化先求前缀和,再枚举所有子数组区间O(n²)O(n)n ≤ 5000 时勉强可用
分治算法将数组分为左右两半,递归求解跨中点的最大子数组O(n log n)O(log n)可作为思维拓展,但编码复杂
Kadane 算法(当前解法)动态规划状态压缩,一次遍历O(n)  ✅O(1)  ✅本题最优解

最优解优化思路

当前 Kadane 算法已经是时间复杂度 O(n)、空间复杂度 O(1) 的最优解。不存在更优的渐进复杂度(因为至少需要遍历所有元素一次)。

完整优化后代码(与原始代码一致,增加防御性)

javascript

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    // 防御性处理(题目已保证非空,但保留增强健壮性)
    if (!nums || nums.length === 0) {
        return 0;
    }
    
    let currentSum = nums[0];
    let maxSum = nums[0];
    
    for (let i = 1; i < nums.length; i++) {
        // 核心状态转移:要么继续累加,要么重新开始
        currentSum = Math.max(nums[i], currentSum + nums[i]);
        // 维护全局最大值
        maxSum = Math.max(maxSum, currentSum);
    }
    
    return maxSum;
};

进一步拓展:如何获取最大子数组本身?

如果题目进阶要求返回最大子数组的起始和结束索引,可以增加两个变量记录:

javascript

var maxSubArrayWithIndex = function(nums) {
    let currentSum = nums[0], maxSum = nums[0];
    let start = 0, end = 0, tempStart = 0;
    
    for (let i = 1; i < nums.length; i++) {
        if (nums[i] > currentSum + nums[i]) {
            currentSum = nums[i];
            tempStart = i; // 重新开始,记录新起点
        } else {
            currentSum = currentSum + nums[i];
        }
        
        if (currentSum > maxSum) {
            maxSum = currentSum;
            start = tempStart;
            end = i;
        }
    }
    
    return { maxSum, subarray: nums.slice(start, end + 1) };
};

这个变体展示了如何将状态转移与索引记录相结合,但核心思想依然是 Kadane 算法。