💥 暴力解 LeetCode 导致 OOM:10000 行数据撑爆 V8 堆内存的完整排查

💥 暴力解 LeetCode 导致 OOM:10000 行数据撑爆 V8 堆内存的完整排查

摘要 :一道 LeetCode 中等题,我自信满满写了双重循环暴力解,结果提交后直接 FATAL ERROR: heap out of memory。本文记录我从"堆爆炸"到理解 V8 内存机制,再到掌握 Kadane 算法和分治法的完整心路历程。


📌 前言

你以为暴力解只是慢?不,它可能直接让 Node.js 崩溃。

昨天刷 LeetCode 53. 最大子数组和,我心想这不简单嘛,双重循环暴力枚举,稳了。结果提交后------暴力解,真的会"爆炸"

事情是这样的:题目要求找出一个整数数组中具有最大和的连续子数组。我心想,这不简单嘛,双重循环枚举所有子数组,把和都存起来,最后排序取最大值,完美!

结果提交后,控制台给我甩了一堆天书般的报错,最后一行赫然写着:

objectivec 复制代码
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

我的代码,把 Node.js 的堆内存给撑爆了。

这篇文章,就记录一下我从"这啥玩意?"到"原来如此!"的完整解题过程,顺便把 V8 引擎的内存机制、堆栈区别、Kadane 算法这些知识点一次性讲透。


🎯 本文适合谁

  • 正在刷力扣、对算法复杂度概念模糊的同学
  • 想了解 JavaScript/V8 引擎内存管理机制的前端开发者
  • 对"堆"和"栈"傻傻分不清楚的同学
  • 想掌握 Kadane 算法(动态规划入门经典)的同学

🐛 我的暴力解(反面教材)

先看看我最初写的"爆炸代码":

javascript 复制代码
var maxSubArray = function(nums) {
    const len = nums.length;
    let arr = [];
    for (let i = 0; i < len; i++) {
        let sum = 0;
        for (let j = i; j < len; j++) {
            sum += nums[j];
            arr.push(sum);  // 💣 问题根源:把所有子数组的和都存起来
        }
    }
    arr.sort((a, b) => a - b);  // 💣 还要排序,雪上加霜
    return arr[arr.length - 1];
};

思路很朴素:枚举所有子数组 → 存到一个大数组里 → 排序 → 取最大值。

看起来没毛病对吧?但当测试用例是一个长度为 10000 的数组时,灾难就来了。


💥 堆爆炸:到底发生了什么?

报错信息拆解

sql 复制代码
<--- Last few GCs --->
[22:0x1550b000] 1519 ms: Mark-Compact (reduce) 390.0 (399.5) -> 390.0 (392.5) MB
  last resort; GC in old space requested

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

翻译成人话

  1. Mark-Compact (reduce):V8 引擎正在执行最耗时的"标记-压缩"垃圾回收(终极手段)
  2. 390.0 (399.5) -> 390.0 (392.5) MB :回收前总内存 399.5 MB,存活对象 390 MB;回收后只腾出了 2.5 MB。GC 忙活了半天,几乎没释放出任何内存
  3. last resort:最后的挣扎,引擎已经用尽所有常规回收手段
  4. FATAL ERROR :正式宣告------内存分配失败,进程阵亡

数学层面:为什么 O(n²) 空间会炸?

对于长度为 n 的数组,连续子数组的总数是:n × (n + 1) / 2

数组长度 n 子数组数量 存储预估(8字节/数字)
1,000 ~50 万 ~4 MB
10,000 ~5,000 万 ~400 MB
100,000 ~50 亿 ~40 GB(直接不可能)

n = 10000 时,光是存这些数字就要 400 MB ,再加上 sort() 底层用的是 TimSort(归并排序变种),需要额外申请一块等大的临时内存用于合并操作。

致命链条 :400MB 存储 + 400MB 排序临时空间 = 800MB → 突破 Node.js 默认堆上限(~1.4GB)→ GC 拼命压缩但 arr 始终被引用,根本回收不了 → 💥 崩溃。


🧠 知识点:堆内存 vs 栈内存

这次爆炸让我彻底搞懂了"堆"和"栈"的区别,用一个比喻来说:栈就像"便利贴"(随写随撕),堆就像"大仓库"(需要专门管理)。

对比表

对比维度 栈内存 (Stack) 堆内存 (Heap)
存储内容 基本数据类型(number、string、boolean 等)、函数调用帧 引用类型(对象 {}、数组 []、函数等)
管理方式 自动分配与释放,LIFO 原则 动态分配,由 GC(垃圾回收器)管理
空间大小 极小且固定(几 MB) 非常大(Node.js 默认 ~1.4GB,可调)
速度 极快(CPU 寄存器直接操作) 较慢(涉及 GC 暂停、内存碎片整理)
报错特征 Maximum call stack size exceeded JavaScript heap out of memory

结合我的代码理解

javascript 复制代码
let arr = [];  // arr 这个变量名 → 存在栈里(只是一个"门牌号")
arr.push(数据); // arr 里的所有元素 → 存在堆里(实体货物全在仓库)

arr 大到 390 MB 且一直被引用时,仓库(堆)被占满了,GC 拼命压缩也腾不出空间,最后 Node.js 直接崩溃------这完全是堆内存的事,和栈没有半毛钱关系。

两种经典崩溃场景

javascript 复制代码
// 1. 栈溢出(无限递归)
function a() { a(); }
a(); // 报错:Maximum call stack size exceeded(栈爆了)

// 2. 堆溢出(大数组占满内存)
let arr = [];
while (true) { arr.push(new Array(1000000)); }
// 报错:heap out of memory(堆爆了,正是我遇到的)

🔧 改进方案:从暴力到优雅

方案一:暴力优化(不存数组,空间 O(1))

如果面试时被要求先说暴力思路,至少别把所有和都存起来,用"在线比较法":

javascript 复制代码
var maxSubArray = function(nums) {
    const len = nums.length;
    let maxSum = -Infinity;
    for (let i = 0; i < len; i++) {
        let sum = 0;
        for (let j = i; j < len; j++) {
            sum += nums[j];
            if (sum > maxSum) maxSum = sum;  // 直接比较,不存数组
        }
    }
    return maxSum;
};
// 时间 O(n²),空间 O(1) ------ 不会爆内存,但会超时

⚠️ 这个写法空间复杂度 O(1),不会爆内存了,但时间复杂度 O(n²) 在 LeetCode 上会超时(TLE)。所以最终提交一定要用下面的最优解。

方案二:Kadane 算法(最优解 ✅)

核心思想:一次遍历,判断"是继续累加当前元素,还是从当前元素重新开始"。

javascript 复制代码
var maxSubArray = function(nums) {
    let maxSum = nums[0];      // 全局最大值
    let currentSum = nums[0];  // 当前累计和

    for (let i = 1; i < nums.length; i++) {
        // 核心决策:继续累加 vs 重新开始
        currentSum = Math.max(nums[i], currentSum + nums[i]);
        // 更新全局最大值
        maxSum = Math.max(maxSum, currentSum);
    }

    return maxSum;
};
// 时间 O(n),空间 O(1) ------ 完美!

执行过程图解

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

步骤 i numsi currentSum 决策 currentSum maxSum
0 -2 初始化 -2 -2
1 1 max(1, -2+1=-1) = 1(重新开始) 1 1
2 -3 max(-3, 1-3=-2) = -2(继续累加) -2 1
3 4 max(4, -2+4=2) = 4(重新开始) 4 4
4 -1 max(-1, 4-1=3) = 3(继续累加) 3 4
5 2 max(2, 3+2=5) = 5(继续累加) 5 5
6 1 max(1, 5+1=6) = 6(继续累加) 6 6
7 -5 max(-5, 6-5=1) = 1(继续累加) 1 6
8 4 max(4, 1+4=5) = 5(继续累加) 5 6

最终结果maxSum = 6,对应子数组 [4, -1, 2, 1]

方案三:分治法(进阶思路 ✅)

除了 Kadane 算法,还有一种经典解法------分治法(Divide and Conquer)。虽然这道题最优解是 Kadane,但分治的思想在面试中经常被追问,而且它能帮你理解"最大子数组和"这个问题的结构。

核心思想 :把数组一分为二,最大子数组和只可能出现在三种位置------左半部分右半部分横跨中点。递归求解,取三者最大值。

javascript 复制代码
var maxSubArray = function(nums) {
    // 辅助递归函数:计算区间 [left, right] 内的最大子数组和
    function divideAndConquer(left, right) {
        // 1. 递归终止条件:区间内只有一个元素
        if (left === right) {
            return nums[left];
        }

        // 2. 分:计算中点
        const mid = Math.floor((left + right) / 2);

        // 3. 治:递归获取左、右半部分的最大值
        const leftMax = divideAndConquer(left, mid);
        const rightMax = divideAndConquer(mid + 1, right);

        // 4. 合:计算横跨中点 (mid) 的最大和
        // 4.1 从中点向左扩展,找最大累加和(必须包含 nums[mid])
        let leftCrossSum = -Infinity;
        let currentLeftSum = 0;
        for (let i = mid; i >= left; i--) {
            currentLeftSum += nums[i];
            if (currentLeftSum > leftCrossSum) {
                leftCrossSum = currentLeftSum;
            }
        }

        // 4.2 从中点向右扩展,找最大累加和(必须包含 nums[mid + 1])
        let rightCrossSum = -Infinity;
        let currentRightSum = 0;
        for (let i = mid + 1; i <= right; i++) {
            currentRightSum += nums[i];
            if (currentRightSum > rightCrossSum) {
                rightCrossSum = currentRightSum;
            }
        }

        const crossMax = leftCrossSum + rightCrossSum;

        // 5. 返回三者中的最大值
        return Math.max(leftMax, rightMax, crossMax);
    }

    // 边界处理
    if (nums.length === 0) return 0;
    return divideAndConquer(0, nums.length - 1);
};
// 时间 O(n log n),空间 O(log n)(递归栈)

分治法图解

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

css 复制代码
                    [-2, 1, -3, 4, -1, 2, 1, -5, 4]
                              ↙        ↓        ↘
                     [-2,1,-3,4]    [横跨中点]   [-1,2,1,-5,4]
                       ↙    ↘                    ↙      ↘
                  [-2,1]  [-3,4]            [-1,2]    [1,-5,4]
                  ↙  ↘    ↙  ↘              ↙  ↘      ↙    ↘
               [-2] [1] [-3] [4]         [-1] [2]  [1]  [-5,4]
  • 左半部分最大值 :递归得到 4(子数组 [4]
  • 右半部分最大值 :递归得到 6(子数组 [2,1][-1,2,1] 等)
  • 横跨中点最大值 :从 mid 向左取 4,向右取 2,合计 6

最终结果max(4, 6, 6) = 6

分治法 vs Kadane:怎么选?

维度 Kadane 算法 分治法
时间复杂度 O(n) O(n log n)
空间复杂度 O(1) O(log n)(递归栈)
代码难度 简单(5 行核心) 中等(30+ 行)
面试价值 最优解,必须掌握 展示分治思想,常被追问
适用场景 直接求解 适合举一反三(如最近点对、归并排序)

💡 面试建议:先说 Kadane 算法(最优解),如果面试官追问"还有没有其他解法",再讲分治法。这样既展示了你知道最优解,又展示了你的算法思维广度。


💡 经验教训

1. 做题前先算复杂度

LeetCode 的硬约束(数组长度可达 10^5)决定了 O(n²) 空间必死无疑。

以后做题,先估算一下:

  • 数据规模 10^3 → O(n²) 能过
  • 数据规模 10^5 → O(n²) 大概率超时,O(n log n) 才安全
  • 数据规模 10^6+ → 必须 O(n) 或 O(n log n)

2. 涉及"所有组合"大概率需要优化

如果题目涉及"所有子数组"、"所有子序列"、"所有排列组合",千万别傻乎乎地全存起来 。大概率需要用动态规划(DP)贪心来优化掉存储过程。

3. 理解 V8 内存机制很重要

  • 栈内存:存基本类型和函数调用,自动管理,极小极快
  • 堆内存:存引用类型(对象、数组),由 GC 管理,大但慢
  • GC 不是万能的:如果对象一直被引用,GC 根本回收不了

4. 遇到 OOM 怎么排查?

  • 应急node --max-old-space-size=4096 your_script.js 临时扩大内存上限(治标不治本)
  • 根治:检查代码中是否有一口气加载全量的操作,改成流式处理
  • 排查:使用 Chrome DevTools 连接 Node 进程,抓取 Heap Snapshot 对比,找到内存占用大户

📊 四种解法对比

解法 时间复杂度 空间复杂度 能否通过 备注
暴力(存所有和) O(n²) O(n²) ❌ 堆爆炸 我最初的写法
暴力(在线比较) O(n²) O(1) ❌ 超时 改进版,不爆内存但超时
分治法 O(n log n) O(log n) ✅ 通过 进阶思路,展示分治思想
Kadane 算法 O(n) O(1) ✅ 通过 最优解,一次遍历

🎤 面试中如何讲解这道题?

面试官问到最大子数组和时,建议按这个节奏讲:

  1. 先说暴力思路 (展示基本功):双重循环枚举所有子数组,O(n²) 时间 O(1) 空间。注意:别说"把所有和存起来再排序",这会暴露你没考虑空间复杂度。
  2. 主动指出暴力的缺陷(展示思考深度):时间复杂度太高,有没有更优的解法?
  3. 引出 Kadane 算法(展示核心能力):核心思想是"如果前面的累加是负收益,果断抛弃,从当前元素重新开始",一次遍历 O(n) 搞定。
  4. 主动补充分治法(展示算法广度):如果面试官追问"还有没有其他思路",可以讲分治法------把数组一分为二,最大子数组和只可能在左半部分、右半部分或横跨中点,递归求解 O(n log n)。
  5. 举一个具体例子走一遍 (展示表达能力):用 [-2,1,-3,4,-1,2,1,-5,4] 走一遍执行过程,让面试官跟着你的思路走。

💡 加分项 :如果面试官追问,可以提到这道题本质上是动态规划 的简化版------状态转移方程就是 dp[i] = max(nums[i], dp[i-1] + nums[i]),只是因为只依赖前一个状态,所以可以用一个变量代替数组,空间优化到 O(1)。


📌 核心收获(四句话总结)

  1. 做题前先算复杂度:数据规模超过 10^4 时,O(n²) 空间必死无疑,涉及"所有组合"大概率需要 DP 或贪心优化。
  2. 暴力解也要写对:就算先说暴力思路,也别把所有中间结果存起来------在线比较、即时更新最大值,空间 O(1) 才是合格的暴力。
  3. 一题多解是加分项:Kadane 算法是最优解 O(n),分治法是进阶思路 O(n log n),面试时主动展示多种解法能体现算法思维广度。
  4. 理解 V8 内存机制:数组存堆不存栈,GC 回收不了被引用的对象,OOM 时先排查谁在"持有"那些大对象。

🔗 参考资料


💬 交流讨论

你有没有遇到过类似的"暴力解翻车"经历?你平时刷 LeetCode 用什么语言?欢迎在评论区分享你的故事和解法!

如果这篇文章帮到了你,点个赞👍收藏⭐关注👆,后续会持续分享更多算法踩坑经验和前端底层原理!


🏷️ 推荐标签前端开发 · JavaScript · 算法 · LeetCode · V8引擎 · 分治法

markdown 复制代码
- **排查**:使用 Chrome DevTools 连接 Node 进程,抓取 Heap Snapshot 对比,找到内存占用大户
复制代码
yaml 复制代码
---
复制代码
shell 复制代码
## 📊 四种解法对比
复制代码
​

| 解法 | 时间复杂度 | 空间复杂度 | 能否通过 | 备注 |
ruby 复制代码
|:---:|:---:|:---:|:---:|:---|
scss 复制代码
| 暴力(存所有和) | O(n²) | O(n²) | ❌ 堆爆炸 | 我最初的写法 |
scss 复制代码
| 暴力(在线比较) | O(n²) | O(1) | ❌ 超时 | 改进版,不爆内存但超时 |
scss 复制代码
| 分治法 | O(n log n) | O(log n) | ✅ 通过 | 进阶思路,展示分治思想 |
scss 复制代码
| Kadane 算法 | O(n) | O(1) | ✅ 通过 | 最优解,一次遍历 |
复制代码
yaml 复制代码
---
复制代码
shell 复制代码
## 🎤 面试中如何讲解这道题?
复制代码
​

面试官问到最大子数组和时,建议按这个节奏讲:

​
scss 复制代码
1. **先说暴力思路**(展示基本功):双重循环枚举所有子数组,O(n²) 时间 O(1) 空间。**注意:别说"把所有和存起来再排序",这会暴露你没考虑空间复杂度。**
markdown 复制代码
2. **主动指出暴力的缺陷**(展示思考深度):时间复杂度太高,有没有更优的解法?
markdown 复制代码
3. **引出 Kadane 算法**(展示核心能力):核心思想是"如果前面的累加是负收益,果断抛弃,从当前元素重新开始",一次遍历 O(n) 搞定。
bash 复制代码
4. **主动补充分治法**(展示算法广度):如果面试官追问"还有没有其他思路",可以讲分治法------把数组一分为二,最大子数组和只可能在左半部分、右半部分或横跨中点,递归求解 O(n log n)。
markdown 复制代码
5. **举一个具体例子走一遍**(展示表达能力):用 `[-2,1,-3,4,-1,2,1,-5,4]` 走一遍执行过程,让面试官跟着你的思路走。
复制代码
scss 复制代码
> 💡 **加分项**:如果面试官追问,可以提到这道题本质上是**动态规划**的简化版------状态转移方程就是 `dp[i] = max(nums[i], dp[i-1] + nums[i])`,只是因为只依赖前一个状态,所以可以用一个变量代替数组,空间优化到 O(1)。
复制代码
yaml 复制代码
---
复制代码
shell 复制代码
## 📌 核心收获(四句话总结)
复制代码
markdown 复制代码
1. **做题前先算复杂度**:数据规模超过 10^4 时,O(n²) 空间必死无疑,涉及"所有组合"大概率需要 DP 或贪心优化。
markdown 复制代码
2. **暴力解也要写对**:就算先说暴力思路,也别把所有中间结果存起来------在线比较、即时更新最大值,空间 O(1) 才是合格的暴力。

💥 暴力解 LeetCode 导致 OOM:10000 行数据撑爆 V8 堆内存的完整排查

摘要 :一道 LeetCode 中等题,我自信满满写了双重循环暴力解,结果提交后直接 FATAL ERROR: heap out of memory。本文记录我从"堆爆炸"到理解 V8 内存机制,再到掌握 Kadane 算法和分治法的完整心路历程。


📌 前言

你以为暴力解只是慢?不,它可能直接让 Node.js 崩溃。

昨天刷 LeetCode 53. 最大子数组和,我心想这不简单嘛,双重循环暴力枚举,稳了。结果提交后------暴力解,真的会"爆炸"

事情是这样的:题目要求找出一个整数数组中具有最大和的连续子数组。我心想,这不简单嘛,双重循环枚举所有子数组,把和都存起来,最后排序取最大值,完美!

结果提交后,控制台给我甩了一堆天书般的报错,最后一行赫然写着:

objectivec 复制代码
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

我的代码,把 Node.js 的堆内存给撑爆了。

这篇文章,就记录一下我从"这啥玩意?"到"原来如此!"的完整解题过程,顺便把 V8 引擎的内存机制、堆栈区别、Kadane 算法这些知识点一次性讲透。


🎯 本文适合谁

  • 正在刷力扣、对算法复杂度概念模糊的同学
  • 想了解 JavaScript/V8 引擎内存管理机制的前端开发者
  • 对"堆"和"栈"傻傻分不清楚的同学
  • 想掌握 Kadane 算法(动态规划入门经典)的同学

🐛 我的暴力解(反面教材)

先看看我最初写的"爆炸代码":

ini 复制代码
var maxSubArray = function(nums) {
    const len = nums.length;
    let arr = [];
    for (let i = 0; i < len; i++) {
        let sum = 0;
        for (let j = i; j < len; j++) {
            sum += nums[j];
            arr.push(sum);  // 💣 问题根源:把所有子数组的和都存起来
        }
    }
    arr.sort((a, b) => a - b);  // 💣 还要排序,雪上加霜
    return arr[arr.length - 1];
};

思路很朴素:枚举所有子数组 → 存到一个大数组里 → 排序 → 取最大值。

看起来没毛病对吧?但当测试用例是一个长度为 10000 的数组时,灾难就来了。

!屏幕截图 2026-07-17 003317(E:\OneDrive\OneDrive\图片\Screenshots\屏幕截图 2026-07-17 003317.png)


💥 堆爆炸:到底发生了什么?

报错信息拆解

sql 复制代码
<--- Last few GCs --->
[22:0x1550b000] 1519 ms: Mark-Compact (reduce) 390.0 (399.5) -> 390.0 (392.5) MB
  last resort; GC in old space requested

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

翻译成人话

  1. Mark-Compact (reduce) :V8 引擎正在执行最耗时的"标记-压缩"垃圾回收(终极手段)
  2. 390.0 (399.5) -> 390.0 (392.5) MB :回收前总内存 399.5 MB,存活对象 390 MB;回收后只腾出了 2.5 MB。GC 忙活了半天,几乎没释放出任何内存
  3. last resort:最后的挣扎,引擎已经用尽所有常规回收手段
  4. FATAL ERROR :正式宣告------内存分配失败,进程阵亡

数学层面:为什么 O(n²) 空间会炸?

对于长度为 n 的数组,连续子数组的总数是:n × (n + 1) / 2

数组长度 n 子数组数量 存储预估(8字节/数字)
1,000 ~50 万 ~4 MB
10,000 ~5,000 万 ~400 MB
100,000 ~50 亿 ~40 GB(直接不可能)

n = 10000 时,光是存这些数字就要 400 MB ,再加上 sort() 底层用的是 TimSort(归并排序变种) ,需要额外申请一块等大的临时内存用于合并操作。

致命链条 :400MB 存储 + 400MB 排序临时空间 = 800MB → 突破 Node.js 默认堆上限(~1.4GB)→ GC 拼命压缩但 arr 始终被引用,根本回收不了 → 💥 崩溃。


🧠 知识点:堆内存 vs 栈内存

这次爆炸让我彻底搞懂了"堆"和"栈"的区别,用一个比喻来说:栈就像"便利贴"(随写随撕),堆就像"大仓库"(需要专门管理)。

对比表

对比维度 栈内存 (Stack) 堆内存 (Heap)
存储内容 基本数据类型(number、string、boolean 等)、函数调用帧 引用类型(对象 {}、数组 []、函数等)
管理方式 自动分配与释放,LIFO 原则 动态分配,由 GC(垃圾回收器)管理
空间大小 极小且固定(几 MB) 非常大(Node.js 默认 ~1.4GB,可调)
速度 极快(CPU 寄存器直接操作) 较慢(涉及 GC 暂停、内存碎片整理)
报错特征 Maximum call stack size exceeded JavaScript heap out of memory

结合我的代码理解

ini 复制代码
let arr = [];  // arr 这个变量名 → 存在栈里(只是一个"门牌号")
arr.push(数据); // arr 里的所有元素 → 存在堆里(实体货物全在仓库)

arr 大到 390 MB 且一直被引用时,仓库(堆)被占满了,GC 拼命压缩也腾不出空间,最后 Node.js 直接崩溃------这完全是堆内存的事,和栈没有半毛钱关系。

两种经典崩溃场景

javascript 复制代码
// 1. 栈溢出(无限递归)
function a() { a(); }
a(); // 报错:Maximum call stack size exceeded(栈爆了)

// 2. 堆溢出(大数组占满内存)
let arr = [];
while (true) { arr.push(new Array(1000000)); }
// 报错:heap out of memory(堆爆了,正是我遇到的)

🔧 改进方案:从暴力到优雅

方案一:暴力优化(不存数组,空间 O(1))

如果面试时被要求先说暴力思路,至少别把所有和都存起来,用"在线比较法":

ini 复制代码
var maxSubArray = function(nums) {
    const len = nums.length;
    let maxSum = -Infinity;
    for (let i = 0; i < len; i++) {
        let sum = 0;
        for (let j = i; j < len; j++) {
            sum += nums[j];
            if (sum > maxSum) maxSum = sum;  // 直接比较,不存数组
        }
    }
    return maxSum;
};
// 时间 O(n²),空间 O(1) ------ 不会爆内存,但会超时

⚠️ 这个写法空间复杂度 O(1),不会爆内存了,但时间复杂度 O(n²) 在 LeetCode 上会超时(TLE) 。所以最终提交一定要用下面的最优解。

方案二:Kadane 算法(最优解 ✅)

核心思想:一次遍历,判断"是继续累加当前元素,还是从当前元素重新开始"。

ini 复制代码
var maxSubArray = function(nums) {
    let maxSum = nums[0];      // 全局最大值
    let currentSum = nums[0];  // 当前累计和

    for (let i = 1; i < nums.length; i++) {
        // 核心决策:继续累加 vs 重新开始
        currentSum = Math.max(nums[i], currentSum + nums[i]);
        // 更新全局最大值
        maxSum = Math.max(maxSum, currentSum);
    }

    return maxSum;
};
// 时间 O(n),空间 O(1) ------ 完美!

执行过程图解

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

步骤 i numsi currentSum 决策 currentSum maxSum
0 -2 初始化 -2 -2
1 1 max(1, -2+1=-1) = 1(重新开始) 1 1
2 -3 max(-3, 1-3=-2) = -2(继续累加) -2 1
3 4 max(4, -2+4=2) = 4(重新开始) 4 4
4 -1 max(-1, 4-1=3) = 3(继续累加) 3 4
5 2 max(2, 3+2=5) = 5(继续累加) 5 5
6 1 max(1, 5+1=6) = 6(继续累加) 6 6
7 -5 max(-5, 6-5=1) = 1(继续累加) 1 6
8 4 max(4, 1+4=5) = 5(继续累加) 5 6

最终结果maxSum = 6,对应子数组 [4, -1, 2, 1]

方案三:分治法(进阶思路 ✅)

除了 Kadane 算法,还有一种经典解法------分治法(Divide and Conquer) 。虽然这道题最优解是 Kadane,但分治的思想在面试中经常被追问,而且它能帮你理解"最大子数组和"这个问题的结构。

核心思想 :把数组一分为二,最大子数组和只可能出现在三种位置------左半部分右半部分横跨中点。递归求解,取三者最大值。

ini 复制代码
var maxSubArray = function(nums) {
    // 辅助递归函数:计算区间 [left, right] 内的最大子数组和
    function divideAndConquer(left, right) {
        // 1. 递归终止条件:区间内只有一个元素
        if (left === right) {
            return nums[left];
        }

        // 2. 分:计算中点
        const mid = Math.floor((left + right) / 2);

        // 3. 治:递归获取左、右半部分的最大值
        const leftMax = divideAndConquer(left, mid);
        const rightMax = divideAndConquer(mid + 1, right);

        // 4. 合:计算横跨中点 (mid) 的最大和
        // 4.1 从中点向左扩展,找最大累加和(必须包含 nums[mid])
        let leftCrossSum = -Infinity;
        let currentLeftSum = 0;
        for (let i = mid; i >= left; i--) {
            currentLeftSum += nums[i];
            if (currentLeftSum > leftCrossSum) {
                leftCrossSum = currentLeftSum;
            }
        }

        // 4.2 从中点向右扩展,找最大累加和(必须包含 nums[mid + 1])
        let rightCrossSum = -Infinity;
        let currentRightSum = 0;
        for (let i = mid + 1; i <= right; i++) {
            currentRightSum += nums[i];
            if (currentRightSum > rightCrossSum) {
                rightCrossSum = currentRightSum;
            }
        }

        const crossMax = leftCrossSum + rightCrossSum;

        // 5. 返回三者中的最大值
        return Math.max(leftMax, rightMax, crossMax);
    }

    // 边界处理
    if (nums.length === 0) return 0;
    return divideAndConquer(0, nums.length - 1);
};
// 时间 O(n log n),空间 O(log n)(递归栈)

分治法图解

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

css 复制代码
                    [-2, 1, -3, 4, -1, 2, 1, -5, 4]
                              ↙        ↓        ↘
                     [-2,1,-3,4]    [横跨中点]   [-1,2,1,-5,4]
                       ↙    ↘                    ↙      ↘
                  [-2,1]  [-3,4]            [-1,2]    [1,-5,4]
                  ↙  ↘    ↙  ↘              ↙  ↘      ↙    ↘
               [-2] [1] [-3] [4]         [-1] [2]  [1]  [-5,4]
  • 左半部分最大值 :递归得到 4(子数组 [4]
  • 右半部分最大值 :递归得到 6(子数组 [2,1][-1,2,1] 等)
  • 横跨中点最大值 :从 mid 向左取 4,向右取 2,合计 6

最终结果max(4, 6, 6) = 6

分治法 vs Kadane:怎么选?

维度 Kadane 算法 分治法
时间复杂度 O(n) O(n log n)
空间复杂度 O(1) O(log n)(递归栈)
代码难度 简单(5 行核心) 中等(30+ 行)
面试价值 最优解,必须掌握 展示分治思想,常被追问
适用场景 直接求解 适合举一反三(如最近点对、归并排序)

💡 面试建议:先说 Kadane 算法(最优解),如果面试官追问"还有没有其他解法",再讲分治法。这样既展示了你知道最优解,又展示了你的算法思维广度。


💡 经验教训

1. 做题前先算复杂度

LeetCode 的硬约束(数组长度可达 10^5)决定了 O(n²) 空间必死无疑。

以后做题,先估算一下:

  • 数据规模 10^3 → O(n²) 能过
  • 数据规模 10^5 → O(n²) 大概率超时,O(n log n) 才安全
  • 数据规模 10^6+ → 必须 O(n) 或 O(n log n)

2. 涉及"所有组合"大概率需要优化

如果题目涉及"所有子数组"、"所有子序列"、"所有排列组合",千万别傻乎乎地全存起来 。大概率需要用动态规划(DP)贪心来优化掉存储过程。

3. 理解 V8 内存机制很重要

相关推荐
小小测试开发1 小时前
Midscene.js:14K Stars 视驱 UI 自动化框架,用截图替代 DOM 选择器写测试
javascript·ui·自动化
cheems95271 小时前
[算法手记]二叉树中序,层序遍历,统计树高
算法
Mh1 小时前
程序员和非程序员使用 AI 进行编程的区别究竟在哪里?
前端·javascript·xcode
wdfk_prog8 小时前
嵌入式面试真题第 10 题:高优化等级下共享状态可见性、内存模型与系统级同步设计
java·linux·开发语言·面试·职场和发展·架构·c
胡萝卜术10 小时前
RAG 的知识炼油厂:从原始网页到可检索知识块的工程化之路
面试·架构·github
wdfk_prog10 小时前
嵌入式面试真题第 11 题:RTOS 优先级翻转与实时任务阻塞的通用治理
c语言·缓存·面试·职场和发展·架构
kyriewen11 小时前
AI 写的 React 组件,合并前必查的 6 个坏味道——每个都有代码对比
前端·javascript·react.js
多加点辣也没关系12 小时前
JavaScript|第13章:原始类型的方法
开发语言·javascript·ecmascript