LeetCode 238. 除了自身以外数组的乘积|最优解详解(O(n)时间+O(1)空间)

在 LeetCode 数组类题目中,238 题「除了自身以外数组的乘积」是一道经典的中等难度题,核心考察对时间、空间复杂度的优化能力,也是面试中的高频考点。题目要求在不使用除法、O(n) 时间复杂度内完成解题,同时最优解还能做到 O(1) 额外空间(除结果数组外)。本文将从题目分析入手,逐步拆解解题思路,深入解析最优代码,并拓展相关面试考点。

一、题目描述与核心约束

题目原文

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除了 nums[i] 之外其余各元素的乘积。题目数据保证数组 nums 之中任意元素的全部前缀元素和后缀的乘积都在 32 位整数范围内。请不要使用除法,且在 O(n) 时间复杂度内完成此题。

核心约束

  • 时间复杂度:必须 O(n),不能用暴力遍历(O(n²) 会超时);

  • 空间复杂度:尽量优化,最优要求 O(1) 额外空间(结果数组不计入额外空间);

  • 禁用除法:避免处理 0 带来的逻辑复杂问题,同时满足题目硬性要求;

  • 数值范围:无需担心乘积溢出 32 位整数,题目已保证。

示例演示

输入:nums = [1,2,3,4] → 输出:[24,12,8,6]

输入:nums = [-1,1,0,-3,3] → 输出:[0,0,9,0,0]

二、解题思路拆解

要在不使用除法的前提下,计算每个元素除外的乘积,核心思路是「拆分乘积来源」------ 每个元素的结果 = 左侧所有元素的乘积 × 右侧所有元素的乘积。基于这个核心,我们可以从基础思路逐步优化到最优解。

思路 1:前缀积 + 后缀积数组(过渡思路)

最直观的想法是用两个辅助数组,分别存储每个元素的前缀积(左侧乘积)和后缀积(右侧乘积),再将两者相乘得到结果:

  1. 前缀积数组 prefix:prefix[i] 表示 nums[0] 到 nums[i-1] 的乘积(即 nums[i] 左侧所有元素的积);

  2. 后缀积数组 suffix:suffix[i] 表示 nums[i+1] 到 nums[n-1] 的乘积(即 nums[i] 右侧所有元素的积);

  3. 结果 answer[i] = prefix[i] × suffix[i]。

该思路时间复杂度 O(n)(三次线性遍历),但空间复杂度 O(n)(两个辅助数组),不符合「O(1) 额外空间」的优化要求,仅作为理解核心逻辑的过渡。

思路 2:两次遍历 + 结果数组复用(最优解)

核心优化点:复用结果数组存储前缀积,再用一个变量动态记录后缀积,反向遍历更新结果,彻底省去辅助数组的空间开销。

具体步骤拆解:

  1. 初始化结果数组 res,长度与 nums 一致;

  2. 正向遍历计算前缀积:用变量 leftProduct 记录当前元素左侧所有元素的乘积(初始为 1,因为第一个元素左侧无元素),将 leftProduct 赋值给 res[i],再更新 leftProduct(乘上当前 nums[i]),此时 res 数组存储的是每个元素的左侧乘积;

  3. 反向遍历计算最终结果:用变量 rightProduct 记录当前元素右侧所有元素的乘积(初始为 1,因为最后一个元素右侧无元素),将 res[i](左侧乘积)与 rightProduct(右侧乘积)相乘,更新 res[i],再更新 rightProduct(乘上当前 nums[i]);

  4. 返回 res 数组,即为最终结果。

该思路仅用两次线性遍历,时间复杂度 O(n),除结果数组外无额外空间,空间复杂度 O(1),完全满足题目要求,也是面试中推荐的标准答案。

三、最优代码解析(TypeScript)

基于上述最优思路,结合边界处理(空数组),代码实现如下,逐行解析核心逻辑:

typescript 复制代码
function productExceptSelf(nums: number[]): number[] {
  const numsL = nums.length;
  // 边界处理:空数组直接返回空
  if (numsL === 0) {
    return [];
  }

  const res = new Array(nums.length);

  // 第一步:正向遍历,计算左侧乘积并存储到 res
  let leftProduct = 1; // 初始左侧乘积为 1(第一个元素左侧无元素)
  for (let i = 0; i < numsL; i++) {
    res[i] = leftProduct; // 先赋值左侧乘积
    leftProduct *= nums[i]; // 更新左侧乘积,包含当前元素,供下一个元素使用
  }

  // 第二步:反向遍历,计算右侧乘积并与左侧乘积相乘,更新 res
  let rightProduct = 1; // 初始右侧乘积为 1(最后一个元素右侧无元素)
  for (let i = numsL - 1; i >= 0; i--) {
    res[i] *= rightProduct; // 左侧乘积 × 右侧乘积 = 最终结果
    rightProduct *= nums[i]; // 更新右侧乘积,包含当前元素,供上一个元素使用
  }

  return res;
};
      

代码执行流程演示(以 nums = [1,2,3,4] 为例)

  1. 正向遍历(计算左侧乘积):

    • i=0:leftProduct=1 → res[0]=1 → leftProduct=1×1=1;

    • i=1:leftProduct=1 → res[1]=1 → leftProduct=1×2=2;

    • i=2:leftProduct=2 → res[2]=2 → leftProduct=2×3=6;

    • i=3:leftProduct=6 → res[3]=6 → leftProduct=6×4=24;

    • 正向遍历后 res = [1,1,2,6]。

  2. 反向遍历(计算最终结果):

    • i=3:rightProduct=1 → res[3]=6×1=6 → rightProduct=1×4=4;

    • i=2:rightProduct=4 → res[2]=2×4=8 → rightProduct=4×3=12;

    • i=1:rightProduct=12 → res[1]=1×12=12 → rightProduct=12×2=24;

    • i=0:rightProduct=24 → res[0]=1×24=24 → rightProduct=24×1=24;

    • 反向遍历后 res = [24,12,8,6],与预期结果一致。

四、面试延伸与注意事项

1. 常见追问与避坑点

  • Q:为什么不能用除法?

    A:一方面是题目硬性要求,另一方面是除法存在两个致命问题:数组含 0 时除法无意义,需额外处理多个 0 的场景;存在数值溢出风险,且逻辑复杂度高于最优解。

  • Q:如何处理数组含 0 的情况?

    A:最优解无需额外处理!因为左侧/右侧乘积会自然包含 0 的影响(如示例 [ -1,1,0,-3,3 ],正向遍历后 res 存储左侧乘积,反向遍历结合右侧乘积,最终会自动得出 [0,0,9,0,0] 的结果)。

  • Q:空间复杂度 O(1) 的依据是什么?

    A:题目允许结果数组不计入额外空间,最优解仅用了两个变量(leftProduct、rightProduct),无其他辅助空间,因此额外空间复杂度为 O(1)。

2. 同类题目推荐

掌握本题思路后,可拓展练习以下同类题目,强化对「前缀/后缀积」的应用:

  • LeetCode 42. 接雨水(前缀最大值+后缀最大值思路);

  • LeetCode 152. 乘积最大子数组(前缀积+后缀积优化);

  • LeetCode 724. 寻找数组的中心下标(前缀和思路)。

五、总结

LeetCode 238 题的核心是「拆分乘积来源」,最优解通过「两次遍历 + 结果数组复用」,在 O(n) 时间、O(1) 额外空间内完成解题,既满足题目约束,又体现了空间优化的核心思想------复用已有空间,用变量替代辅助数组

这类题目在面试中常考察「思路优化能力」,从暴力法到过渡思路,再到最优解,逐步拆解的过程能帮助我们理解算法优化的本质。掌握该思路后,可快速迁移到前缀和、前缀最大值等同类问题中,提升解题效率。

相关推荐
one____dream2 小时前
【算法】大整数数组连续进位
python·算法
one____dream2 小时前
【算法】合并两个有序链表
数据结构·python·算法·链表
大江东去浪淘尽千古风流人物2 小时前
【Project Aria】Meta新一代的AR眼镜及其数据集
人工智能·嵌入式硬件·算法·性能优化·ar·dsp开发
电饭叔2 小时前
has_solution = False 是什么 费马大定律代码化和定理《计算机科学中的数学》外扩学习3
学习·算法
一只小bit2 小时前
Qt 网络:包含Udp、Tcp、Http三种协议的客户端实践手册
前端·c++·qt·页面
闻缺陷则喜何志丹2 小时前
【动态规划】P9980 [USACO23DEC] Flight Routes G|普及+
c++·算法·动态规划·洛谷
Σίσυφος19002 小时前
视觉矩阵之 正交矩阵
人工智能·算法·矩阵
wen__xvn2 小时前
基础算法集训第21天:Bellman-Ford
算法
zfj3212 小时前
小数和整数10进制转2进制算法
算法·二进制·进制转换·十进制