238. 除了自身以外数组的乘积 — 面试向深度解析

238. 除了自身以外数组的乘积 --- 面试向深度解析


一、读题

一句话复述 :给定整数数组 nums,返回一个新数组 answer,其中每个位置 i 的值等于 nums除了 nums[i] 以外所有元素的乘积

关键约束高亮

约束 含义
2 ≤ nums.length ≤ 10^5 数据量 10 万级O(n²) 必挂
-30 ≤ nums[i] ≤ 30 元素值范围小,但有负数、有 0
不能用除法 直接排除「总乘积 ÷ 当前数」的简单做法
O(n) 时间 + O(1) 额外空间(输出数组不算) 强约束,不能开额外辅助数组

⚠️ 特别注意:题目保证「前缀乘积 × 后缀乘积」在 32 位整数范围内,所以不用担心溢出


二、前置知识

必须掌握的 3 个知识点

序号 知识点 在本題中的作用
前缀积思想 从左到右累计乘积,得到「当前位置左边所有数的乘积」
后缀积思想 从右到左累计乘积,得到「当前位置右边所有数的乘积」
原地复用输出数组 answer 先存前缀积,再乘后缀积,实现 O(1) 额外空间

非常见 API(本题几乎没用特殊 API,以下为通用 Array 操作):

方法 作用 参数 返回值 使用场景
new Array(n) 创建指定长度的空数组 length 空位数组 预分配空间,避免动态扩容
arr[i] 数组索引访问/赋值 index 元素值 O(1) 随机访问

三、思路推导(核心)

🔴 暴力解 ------ 双重循环

javascript 复制代码
var productExceptSelf = function(nums) {
    const n = nums.length;
    const answer = [];
    for (let i = 0; i < n; i++) {
        let product = 1;
        for (let j = 0; j < n; j++) {
            if (i !== j) product *= nums[j];
        }
        answer[i] = product;
    }
    return answer;
};
  • 时间复杂度O(n²) ------ 对于每个位置都要遍历整个数组乘一遍
  • 空间复杂度O(1)(不算输出)
  • 代入最大数据量n = 10^510^10 次乘法,远超 JS 单线程极限(约 1 秒可处理 10^7~10^8 次简单运算),必然超时。

🟡 优化动机 ------ 从哪里想到更好的解法?

暴力痛点 :每个 answer[i] 独立计算,大量重复乘法(nums[0] * nums[1] * ... 反复计算)。

关键观察

对于 answer[i],它其实就是「左边所有数的乘积 × 右边所有数的乘积」。

ini 复制代码
nums = [1, 2, 3, 4]

answer[0] = () × (2×3×4) = 1 × 24 = 24
answer[1] = (1) × (3×4) = 1 × 12 = 12
answer[2] = (1×2) × (4) = 2 × 4 = 8
answer[3] = (1×2×3) × () = 6 × 1 = 6

思维演进路径

  1. 先想「能不能一次性算出所有前缀积?」→ 可以,从左往右扫一遍。
  2. 再想「后缀积是不是也能同理?」→ 可以,从右往左扫一遍。
  3. 最后想「能不能省掉存后缀积的数组?」→ 可以,用一个变量 边走边累乘,直接乘到 answer 上。

这就是两次扫描(Two-Pass) 的由来。


🟢 手把手模拟(示例 nums = [1, 2, 3, 4]

第一趟(左→右) :计算每个位置左侧乘积,存入 answer

索引 操作 answer 当前值 说明
初始 answer[0] = 1 [1, _, _, _] 第一个元素左边没有数,乘积为 1
i=1 answer[1] = answer[0] * nums[0] = 1*1 [1, 1, _, _] nums1 左边是 1
i=2 answer[2] = answer[1] * nums[1] = 1*2 [1, 1, 2, _] nums2 左边是 1,2
i=3 answer[3] = answer[2] * nums[2] = 2*3 [1, 1, 2, 6] nums3 左边是 1,2,3

第二趟(右→左) :用 suffix 累乘右边乘积,乘到 answer

索引 suffix 当前值 操作 answer 更新后
i=3 1 answer[3] = 6 * 1 = 6suffix *= nums[3] → 4 [1, 1, 2, 6]
i=2 4 answer[2] = 2 * 4 = 8suffix *= nums[2] → 12 [1, 1, 8, 6]
i=1 12 answer[1] = 1 * 12 = 12suffix *= nums[1] → 24 [1, 12, 8, 6]
i=0 24 answer[0] = 1 * 24 = 24suffix *= nums[0] → 24 [24, 12, 8, 6]

✅ 得到最终答案 [24, 12, 8, 6]


四、代码与避坑

完整代码(已给,带注释增强版)

javascript 复制代码
var productExceptSelf = function(nums) {
    const n = nums.length;
    const answer = new Array(n);

    // === 第一趟:前缀积(左→右) ===
    answer[0] = 1;                    // 最左侧左边无元素
    for (let i = 1; i < n; i++) {
        // answer[i] = nums[0] * ... * nums[i-1]
        answer[i] = answer[i - 1] * nums[i - 1];
    }

    // === 第二趟:乘上后缀积(右→左) ===
    let suffix = 1;                   // 记录「当前位置右边所有数的乘积」
    for (let i = n - 1; i >= 0; i--) {
        answer[i] = answer[i] * suffix;  // 左边积 × 右边积 = 最终结果
        suffix *= nums[i];               // 向右移动一位,把当前数纳入右边乘积
    }

    return answer;
};

边界攻防表

边界场景 ❌ 错误写法 ✅ 正确写法 原因
数组长度为 2 循环条件 i < n - 1 漏算最后一位 i < n / i >= 0 全覆盖 长度 2 时前缀/后缀各只有一个元素,不能少乘
含有 0 用除法:「总乘积 / numsi 用两次扫描乘法 除数为 0 会报错;且题目明确禁止除法
负数乘积 suffix = 0 初始化 suffix = 1 初始化 累乘必须从单位元 1 开始,0 会"污染"所有结果

五、举一反三

面试追问方向

追问 回答思路
Q1:如果允许使用除法,你会怎么写?有什么风险? 先算总乘积,再 answer[i] = total / nums[i]。风险:① nums[i] = 0 时除零;② 多个 0 时全部为 0,需要特殊处理计数。
Q2:如果数组是流式输入的(不能事先知道长度),怎么办? 流式场景无法一次拿到所有数据,只能先存下来再算,或者用「两次遍历」在输入完成后执行。本质上是离线算法
Q3:如果要求输出结果本身也算空间,如何做到真正 O(1)? 不可能------因为输出数组本身就有 n 个元素,空间至少 O(n)。这里的 O(1) 是**「除输出外」**的额外空间,是业界的常见表述。
Q4:如果数组元素很大,乘积会溢出怎么办? 题目已保证在 32 位范围内;如果没保证,要用 BigInt 或取模运算(如 (a * b) % MOD)。

相似题型推荐

题目 与本题差异 解法变化
LeetCode 42. 接雨水 不是乘积,而是高度差的累积 同样用左右双指针/前缀最值两次扫描,核心思想是「某位置的值由左边最大值和右边最大值共同决定」
LeetCode 135. 分发糖果 不是乘积,而是评分规则 同样用两次扫描(左→右满足左规则,右→左满足右规则),取最大值合并

总结

维度 要点
核心思想 前缀积 × 后缀积 = 除自身外乘积
时间 O(n),两次线性扫描
空间 O(1)(不算输出数组)
关键洞察 answer 暂存前缀积,用一个变量滚动累乘后缀积
面试亮点 能主动说明「为什么不能用除法」「如何处理 0」「空间复杂度的严谨表述」

💡 记忆口诀:左扫存前缀,右扫乘后缀,一变量滚到底,原地输出不用愁。

相关推荐
kisshyshy1 小时前
前端存储、表单异步提交与 this 指向完全指南
前端·javascript·html
Kel1 小时前
管道、IPC、Socket:你以为的三种技术,其实是同一件事
javascript·设计模式·node.js
心中有国也有家1 小时前
AtomGit Flutter 鸿蒙客户端: AnimatedScale 与 AnimatedContainer 联合实战
android·javascript·flutter·华为·harmonyos
浩瀚地学1 小时前
【面试算法笔记】0105-数组-螺旋矩阵
java·开发语言·笔记·算法·面试
嘉泰姆半导体官方1 小时前
CXAR41337C 双通道数字音频功放 | 2×20W | EQ+DRC | I²S | MCLK-Less - 嘉泰姆电子
前端·javascript·数据库
暗不需求1 小时前
目前最火AI协议-> MCP 协议实战:从原理到落地,让 AI 拥有"超能力"
面试·ai编程·mcp
胡萝卜术1 小时前
从逐一合并到多路归并:力扣23「合并K个升序链表」的三种解法进化之路
前端·javascript·面试
名字还没想好☜1 小时前
React 受控与非受控组件:表单场景怎么选
前端·javascript·react.js·react·表单·受控组件
KaifuZeng1 小时前
模电面试问题汇总三
单片机·嵌入式硬件·面试·模电