在 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:前缀积 + 后缀积数组(过渡思路)
最直观的想法是用两个辅助数组,分别存储每个元素的前缀积(左侧乘积)和后缀积(右侧乘积),再将两者相乘得到结果:
-
前缀积数组 prefix:prefix[i] 表示 nums[0] 到 nums[i-1] 的乘积(即 nums[i] 左侧所有元素的积);
-
后缀积数组 suffix:suffix[i] 表示 nums[i+1] 到 nums[n-1] 的乘积(即 nums[i] 右侧所有元素的积);
-
结果 answer[i] = prefix[i] × suffix[i]。
该思路时间复杂度 O(n)(三次线性遍历),但空间复杂度 O(n)(两个辅助数组),不符合「O(1) 额外空间」的优化要求,仅作为理解核心逻辑的过渡。
思路 2:两次遍历 + 结果数组复用(最优解)
核心优化点:复用结果数组存储前缀积,再用一个变量动态记录后缀积,反向遍历更新结果,彻底省去辅助数组的空间开销。
具体步骤拆解:
-
初始化结果数组 res,长度与 nums 一致;
-
正向遍历计算前缀积:用变量 leftProduct 记录当前元素左侧所有元素的乘积(初始为 1,因为第一个元素左侧无元素),将 leftProduct 赋值给 res[i],再更新 leftProduct(乘上当前 nums[i]),此时 res 数组存储的是每个元素的左侧乘积;
-
反向遍历计算最终结果:用变量 rightProduct 记录当前元素右侧所有元素的乘积(初始为 1,因为最后一个元素右侧无元素),将 res[i](左侧乘积)与 rightProduct(右侧乘积)相乘,更新 res[i],再更新 rightProduct(乘上当前 nums[i]);
-
返回 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] 为例)
-
正向遍历(计算左侧乘积):
-
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]。
-
-
反向遍历(计算最终结果):
-
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) 额外空间内完成解题,既满足题目约束,又体现了空间优化的核心思想------复用已有空间,用变量替代辅助数组。
这类题目在面试中常考察「思路优化能力」,从暴力法到过渡思路,再到最优解,逐步拆解的过程能帮助我们理解算法优化的本质。掌握该思路后,可快速迁移到前缀和、前缀最大值等同类问题中,提升解题效率。