数组+dp

对应力扣53最大子数组和

你目前用的暴力枚举所有子数组求和找最大值的思路,逻辑是完全正确的,能直观理解题目要求,但核心问题是效率极低(力扣测试用例直接超时)。下面先讲清暴力法的问题,再重点讲 53 题的最优解 ——Kadane 算法(卡登算法),全程大白话 + 例子 + 代码,从思路到实现一步到位,保证你能懂!

一、先明确 53 题核心要求

给定一个整数数组(包含正数、负数、0),找到一个连续的子数组(子数组是数组中连续的一段元素,不能为空),使得这个子数组的和最大,返回这个最大和。

  • 示例 1:nums = [-2,1,-3,4,-1,2,1,-5,4] → 最大子数组[4,-1,2,1],和为6
  • 示例 2:nums = [1] → 最大和为1
  • 示例 3:nums = [5,4,-1,7,8] → 最大和为23(整个数组)。

二、你的暴力思路:逻辑正确,但必超时

暴力法核心步骤
  1. 枚举所有可能的子数组起始位置i(从 0 到数组末尾);
  2. 对于每个起始位置i,枚举所有可能的结束位置j(从i到数组末尾);
  3. 计算子数组[i,j]的和,用一个变量记录遍历过程中的最大和
  4. 遍历结束后,返回记录的最大和。
暴力法 JavaScript 代码(贴合你的思路)

javascript

运行

var maxSubArray = function(nums) {
    let maxSum = -Infinity; // 初始化为负无穷,兼容全负数情况
    const n = nums.length;
    // 枚举所有起始位置i
    for (let i = 0; i < n; i++) {
        let curSum = 0; // 记录当前子数组的和
        // 枚举所有结束位置j(从i开始,保证连续)
        for (let j = i; j < n; j++) {
            curSum += nums[j]; // 累加得到[i,j]的和
            maxSum = Math.max(maxSum, curSum); // 更新最大值
        }
    }
    return maxSum;
};
暴力法的致命问题:时间复杂度太高

暴力法需要两层嵌套循环,时间复杂度为 O(n2)(n是数组长度)。当力扣测试用例中n很大时(比如n=10^5),计算量会达到1010,直接超时,无法通过所有测试用例。

而且暴力法存在重复计算:比如计算[0,3]的和后,计算[0,4]时又要重新累加,没有利用之前的计算结果,这是效率低的核心原因。

三、最优解:Kadane 算法(卡登算法)—— 时间O(n),空间O(1)

Kadane 算法是解决「最大子数组和」的最优解,核心思想是 **「贪心 + 动态规划」,只用一次遍历 ** 就能找到答案,全程只维护两个简单变量,空间复杂度为O(1),能轻松处理大数据量的测试用例。

核心思路:边走边选,“择优继承” 当前子数组和

把遍历数组的过程,想象成依次走到每个元素的位置,做一个简单的选择

对于当前元素nums[i],它所在的连续子数组,要么把前面的子数组和 “继承” 过来,和自己拼接成新的子数组;要么抛弃前面的所有元素,从自己开始重新建一个子数组。怎么选?选和更大的那个

比如走到元素4(对应示例 1 的nums[3]),前面的子数组和是-2+1-3=-4,此时选择「抛弃前面,从 4 开始」(和为 4),比「继承前面,-4+4=0」更优。

两个核心变量(全程只维护这两个,超简单)
  1. curSum以当前元素为最后一个元素的连续子数组的最大和(记录 “当前选择后的子数组和”);
  2. maxSum遍历过程中全局的最大子数组和(记录 “走到现在为止找到的最大值”)。
核心规则(2 句话记死,算法的核心)

遍历数组中每个元素(从第一个开始),依次执行两个操作:

  1. 更新curSumcurSum = Math.max(nums[i], curSum + nums[i])
    • 解释:二选一 ——「从当前元素重新开始」(和为nums[i]) or 「继承前面的子数组」(和为curSum + nums[i]),选和更大的;
  2. 更新maxSummaxSum = Math.max(maxSum, curSum)
    • 解释:用当前的最优子数组和,更新全局的最大值。
初始化(关键,兼容全负数情况)
  • curSum 初始化为nums[0]:第一个元素的子数组只能是自己,和为自身值;
  • maxSum 初始化为nums[0]:初始的全局最大值就是第一个元素的和。

四、手动模拟经典例子(一步一步走,全程无跳步)

用示例 1 nums = [-2,1,-3,4,-1,2,1,-5,4],模拟 Kadane 算法的执行过程,每一步都清晰看到变量变化,保证你能跟上!初始化:curSum = -2maxSum = -2

遍历元素元素值计算 curSum(二选一)curSum 更新后计算 maxSum(比大小)maxSum 更新后
第 2 个1max(1, -2+1=-1) → 11max(-2, 1) →11
第 3 个-3max(-3, 1+(-3)=-2) →-2-2max(1, -2) →11
第 4 个4max(4, -2+4=2) →44max(1,4) →44
第 5 个-1max(-1,4+(-1)=3) →33max(4,3) →44
第 6 个2max(2,3+2=5) →55max(4,5) →55
第 7 个1max(1,5+1=6) →66max(5,6) →66
第 8 个-5max(-5,6+(-5)=1) →11max(6,1) →66
第 9 个4max(4,1+4=5) →55max(6,5) →66

遍历结束,maxSum=6,和题目预期的答案完全一致!

五、Kadane 算法 JavaScript 代码(极简,5 行核心逻辑)

代码完全遵循上面的规则,极简且高效,直接复制就能通过力扣 53 题所有测试用例(包括全负数、单元素、全正数等边界情况):

javascript

运行

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    // 初始化:第一个元素为初始值,兼容所有边界情况
    let curSum = nums[0];
    let maxSum = nums[0];
    // 从第二个元素开始遍历(第一个已经初始化)
    for (let i = 1; i < nums.length; i++) {
        // 核心1:择优继承,更新当前子数组和
        curSum = Math.max(nums[i], curSum + nums[i]);
        // 核心2:更新全局最大值
        maxSum = Math.max(maxSum, curSum);
    }
    return maxSum;
};

六、边界情况测试(验证算法的鲁棒性)

情况 1:数组只有一个元素 → nums = [1]
  • 初始化:curSum=1maxSum=1
  • 无循环,直接返回1 → 正确。
情况 2:数组全是负数 → nums = [-5,-3,-2,-4]
  • 遍历过程:依次选择 “自己”(因为加前面的负数会更小);
  • 最终maxSum=-2(最大的单个元素) → 正确。
情况 3:数组全是正数 → nums = [5,4,-1,7,8]
  • 遍历过程:一直 “继承前面的和”(加正数会更大);
  • 最终curSum=5+4-1+7+8=23maxSum=23 → 正确。

七、暴力法 vs Kadane 算法 对比(差距一目了然)

方法时间复杂度空间复杂度核心特点能否通过力扣
暴力法O(n2)O(1)逻辑简单,重复计算严重不能(超时)
Kadane 算法O(n)O(1)一次遍历,无重复计算,极简能(最优解)

八、Kadane 算法的本质(可选了解,加深理解)

Kadane 算法本质是动态规划的简化版,核心状态转移方程就是:dp[i]=max(nums[i],dp[i−1]+nums[i])

  • dp[i]:以第i个元素为结尾的最大子数组和(对应代码中的curSum);
  • 因为每次计算dp[i]只需要dp[i-1],所以不用开辟数组存储dp,只用一个变量curSum即可,这就是空间优化,最终得到了极简的 Kadane 算法。

九、核心总结(3 句话记死,直接套用)

  1. Kadane 算法是 53 题的最优解,时间O(n)、空间O(1),秒杀所有测试用例;
  2. 核心只维护两个变量:curSum(当前结尾的最大子数组和)、maxSum(全局最大和);
  3. 核心规则只有两步:curSum = max(当前元素, 前curSum+当前元素)maxSum = max(旧maxSum, 新curSum)

这个算法是子数组 / 子串最值问题的经典算法,不仅适用于 53 题,后续遇到类似的 “连续子数组和 / 最值” 问题,都可以尝试用这个思路解决,记熟之后能快速搞定这类题目~

我看懂代码了,但是不知道这个kadane的原理,为什么每次判断自己or加上前面就能找到最优

你的问题直击 Kadane 算法的核心本质—— 这是一种基于贪心思想的最优选择策略,核心原理是:以当前元素为结尾的连续子数组,其最大和只有两种「唯一可能」的构成方式,且二者取大就是最优解,不存在第三种更优的情况。下面用大白话 + 反证 + 例子彻底讲透这个原理,让你从 “看懂代码” 到 “理解为什么代码能 work”!

一、先明确一个核心前提(所有推导的基础)

我们的目标是找整个数组的最大连续子数组和,而 Kadane 算法的思路是把大问题拆解为无数个小问题

不直接找全局最大,而是先找「以数组中每个元素为最后一个元素的连续子数组的最大和」(记为dp[i],对应代码中的curSum),最终全局最大就是所有dp[i]中的最大值。

比如数组[-2,1,-3,4,-1,2,1,-5,4],我们依次找:

  • -2结尾的最大子数组和 → -2
  • 1结尾的最大子数组和 → 1
  • -3结尾的最大子数组和 → -2
  • 4结尾的最大子数组和 → 4
  • ...最终从这些值里挑最大的(6),就是答案。

为什么能这么拆解?因为任何一个连续子数组,必然有一个最后一个元素,全局最大子数组,一定是「以某个元素为结尾的最大子数组」。就像 “全班最高的同学”,一定是 “某一排的最高同学”,我们只要找到每一排的最高,再挑全局最高就行。

二、核心原理:以当前元素结尾的子数组,只有两种构成方式(无第三种)

假设我们现在遍历到元素nums[i],要找「以nums[i]为最后一个元素的连续子数组的最大和dp[i]」,思考一个问题:

连续子数组以nums[i]结尾,它的前一个元素只能是nums[i-1](因为连续),那这个子数组的构成只有两种可能:

可能 1:「抛弃前面所有元素,从nums[i]自己开始」

子数组就是[nums[i]],和为nums[i]。适用场景:前面的子数组和是负数,加上当前元素会让和变小,不如自己单独成组。比如前面的和是-4,当前元素是4-4+4=0 < 4,选自己更优。

可能 2:「继承前面的最优结果,和nums[i]拼接」

子数组是「以nums[i-1]结尾的最优子数组」+nums[i],和为dp[i-1] + nums[i]dp[i-1]是前一个元素的最优解)。适用场景:前面的子数组和是正数 / 0,加上当前元素会让和变大,拼接更优。比如前面的和是5,当前元素是15+1=6 > 1,选拼接更优。

三、关键结论:二者取大,就是当前的最优解(无第三种更优可能)

为什么这两种情况取大,就是「以nums[i]结尾的最大子数组和」?因为不存在第三种构成方式

  • 连续子数组以nums[i]结尾,要么包含前面的元素,要么不包含;
  • 包含前面的元素时,必须选前面的最优解dp[i-1](如果选前面的非最优解,比如选一个更小的和,拼接后结果会更差,不可能成为当前最优);
  • 不包含前面的元素时,只能选自己。

简单说:这是一道二选一的单选题,没有其他选项,且正确答案就在两个选项中—— 这就是 Kadane 算法中curSum = Math.max(nums[i], curSum + nums[i])的核心由来!

四、用反证法验证:没有比这两种选择更优的情况

假设存在「以nums[i]结尾的子数组,其和比max(nums[i], dp[i-1]+nums[i])更大」,那么这个子数组必然满足:

  1. 包含前面的元素,但没有选dp[i-1](即选了前面的某个非最优子数组);
  2. 设这个非最优子数组的和为x,则x + nums[i] > dp[i-1] + nums[i]x > dp[i-1]

但这与dp[i-1]的定义矛盾dp[i-1]是「以nums[i-1]结尾的最大子数组和」,不可能存在x > dp[i-1])。

因此,不存在第三种更优的构成方式,二选一取大就是唯一的最优解。

五、结合贪心思想:每一步局部最优 → 最终全局最优

Kadane 算法的本质是贪心算法,贪心的核心是「每一步做出局部最优选择,最终得到全局最优」,这里的局部最优就是:

对于每个元素nums[i],只关心「以自己结尾的最优解」,不关心前面的细节,也不关心后面的元素,只做当前能做的最好选择。

而因为我们的核心前提(全局最优一定在局部最优中),这些一步步的局部最优,最终能推导出全局最优 —— 这就是贪心思想在这道题中的完美应用。

六、用经典例子再验证(看局部最优如何推导出全局最优)

还是用nums = [-2,1,-3,4,-1,2,1,-5,4],结合「两种选择」看每一步的局部最优选择,以及如何推导出全局最优:初始化:以-2结尾的局部最优dp[0] = -2,全局最优maxSum = -2

当前元素前一个局部最优dp[i-1]选择 1:自己选择 2:拼接(dp [i-1]+ 自己)局部最优dp[i](二选一)全局最优(更新 / 保留)
1-21-2+1=-111(更新)
-31-31+(-3)=-2-21(保留)
4-24-2+4=244(更新)
-14-14+(-1)=334(保留)
2323+2=555(更新)
1515+1=666(更新)
-56-56+(-5)=116(保留)
4141+4=556(保留)

每一步的局部最优都严格遵循「二选一取大」,而最终的全局最优6,正是所有局部最优中的最大值 —— 完美验证了原理的正确性!

七、再解释一个关键疑问:为什么负数数组也能处理?

比如全负数数组[-5,-3,-2,-4],每一步的选择都是「选自己」(因为拼接前面的负数会更小),最终局部最优依次是-5、-3、-2、-4,全局最优是-2(最大的单个元素),完全符合题意。

这是因为 Kadane 算法的选择策略没有任何前提(不管前面是正还是负,只管二选一取大),自然兼容所有边界情况。

八、核心原理总结(3 句话记死,彻底理解)

  1. 问题拆解:全局最大连续子数组和,一定是「以某个元素为结尾的局部最大子数组和」中的最大值;
  2. 二选一最优:以当前元素结尾的局部最大和,只有两种构成方式 ——「自己单独成组」或「继承前一个局部最优拼接成组」,二者取大就是当前局部最优(无第三种更优可能);
  3. 贪心推导:每一步做局部最优选择,最终所有局部最优的最大值,就是全局最优(贪心思想的完美应用)。

最后:从原理到代码的映射

理解了原理,再看代码就会发现,代码只是把原理直接翻译成代码语言

  • curSum:表示「以当前元素为结尾的局部最大子数组和」(即dp[i]);
  • curSum = Math.max(nums[i], curSum + nums[i]):二选一取大,更新局部最优;
  • maxSum = Math.max(maxSum, curSum):从所有局部最优中,找全局最优。

其实 Kadane 算法的原理一点都不复杂,只是把我们直觉上的 “选更优的方式组成子数组” 变成了严格的、可执行的算法规则—— 这也是算法的魅力:把模糊的直觉,变成清晰的逻辑!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值