1170 字
6 分钟
238. 除了自身以外数组的乘积:把当前位置空出来

本文是 LeetCode 刷题记录 的第四篇题解。

这题的要求是:answer[i] 等于 nums 里除了 nums[i] 以外,其他所有元素的乘积。

题目还特别强调:不要使用除法。

如果能用除法,最直接的想法可能是先把所有数乘起来,再除以当前位置的数。但题目把这条路堵上了。堵得也合理,因为数组里可能有 0,一除就容易现场翻车。面试官看了都要问一句:你这个 0 打算怎么除?

所以这题我现在的理解是:不要想着“总乘积 / 当前元素”,而是把当前位置空出来,看它左边贡献什么、右边贡献什么。

1. 把当前位置当成分界点#

对于位置 i 来说,除了 nums[i] 以外的乘积,可以拆成两部分:

answer[i] = i 左边所有元素的乘积 × i 右边所有元素的乘积

也就是:

answer[i] = L[i] * R[i]

这里的 L[i] 表示 nums[i] 左边所有数的乘积,R[i] 表示 nums[i] 右边所有数的乘积。

这样当前位置 nums[i] 就自然不会参与计算。不是说真的把 nums[i] 改成 1,而是 L[i]R[i] 的定义本身就绕开了它。当前位置像是被临时挖了个洞,左边和右边各算各的,最后在洞口握个手。

2. 为什么边界要放 1#

代码里先设置:

L[0] = 1;
R[length - 1] = 1;

一开始我觉得这里有点像“硬塞一个 1”。后来想明白了:乘法里 1 是单位元,不会改变结果。

对于第 0 个位置来说,它左边没有任何元素,所以左边乘积就应该是 1。这样 answer[0] = L[0] * R[0] 时,真正起作用的是右边所有元素的乘积。

同理,最后一个位置右边没有元素,所以 R[length - 1] = 1

这个 1 的意义不是“这里有一个数字 1”,而是“这一侧没有元素,不影响乘积”。

3. 用 [1,2,3,4] 跑一遍#

先看左边乘积数组 L

nums = [1, 2, 3, 4]
L = [1, 1, 2, 6]

含义是:

  • L[0] = 1,第 0 位左边没有数;
  • L[1] = 1,第 1 位左边只有 1
  • L[2] = 1 * 2 = 2
  • L[3] = 1 * 2 * 3 = 6

再看右边乘积数组 R

R = [24, 12, 4, 1]

含义是:

  • R[0] = 2 * 3 * 4 = 24
  • R[1] = 3 * 4 = 12
  • R[2] = 4
  • R[3] = 1,最后一位右边没有数。

最后左右相乘:

位置nums[i]L[i]R[i]answer[i]
0112424
1211212
23248
34616

所以结果就是:

[24, 12, 8, 6]

这个过程其实很直观:每个位置都不管自己,只拿左边和右边的乘积来拼答案。

4. 代码里的两次遍历#

先从左往右填 L

L[i] = nums[i - 1] * L[i - 1];

这里 L[i] 不乘 nums[i],而是乘 nums[i - 1]。这正好保证 L[i] 只包含当前位置左边的元素。

再从右往左填 R

R[i] = nums[i + 1] * R[i + 1];

同理,R[i] 不乘 nums[i],而是乘 nums[i + 1],保证 R[i] 只包含当前位置右边的元素。

最后再遍历一次:

answer[i] = L[i] * R[i];

左右一合体,当前位置自己就被排除掉了。

5. C++ 写法#

class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int length = nums.size();
vector<int> L(length,0), R(length, 0);
vector<int> answer(length);
L[0] = 1;
for (int i = 1; i < length; i++){
L[i] = nums[i - 1] * L[i - 1];
}
R[length - 1] = 1;
for (int i = length - 2; i >= 0; i--){
R[i] = nums[i + 1] * R[i + 1];
}
for (int i = 0; i < length; i++){
answer[i] = L[i] * R[i];
}
return answer;
}
};

这份代码的结构很清楚:左边一张表,右边一张表,最后合成答案。它没有用除法,所以遇到 0 也不会有除零问题。

比如 nums = [-1,1,0,-3,3],只有当当前位置是 0 时,左边和右边乘起来才会得到非零的 9;其他位置的左边或右边都会包含那个 0,结果自然就是 0。这就是不用除法的好处:不用专门写一堆关于 0 的分支。

6. 总结#

这题我主要记住一个模型:当前位置不参与,答案来自左边乘积和右边乘积。

L[i]R[i] 的存在,就是为了把 nums[i] 从计算里绕过去。边界上的 1 也不是魔法数字,只是乘法单位元,表示这一侧没有元素。

时间复杂度是 O(n),因为数组只被线性遍历了几次;额外空间是 O(n),因为用了 LR 两个辅助数组。后面如果想继续压空间,可以把其中一侧乘积直接放进 answer,但这篇先把最直观的左右拆分想清楚。