前言
这道题最容易想到的办法,大概都是先把数组中所有元素乘起来,再用总乘积除以当前位置的元素。
思路看起来没有问题,题目却直接补了一句:不能使用除法。
如果数组中还可能出现 0,总乘积除以当前元素这条路本身也会变得麻烦。第一次做到这里时,很容易把注意力一直放在"怎样从总乘积中去掉 nums[i]"上。实际上,换一个角度会简单很多:既然不能把当前元素除掉,那就从一开始不让它参与计算。
对于位置 i 来说,除 nums[i] 以外的元素只有两部分:它左边的元素和它右边的元素。因此:
answer[i] = i 左边所有元素的乘积 × i 右边所有元素的乘积
这就是前缀积与后缀积解法的出发点。
一、题目说明
给定一个整数数组 nums,返回数组 answer,其中:
answer[i] = nums 中除 nums[i] 以外其余所有元素的乘积
题目还有两个重要限制:
-
不能使用除法;
-
需要在
O(n)时间复杂度内完成。
例如:
输入:nums = [1,2,3,4]
输出:[24,12,8,6]
其中:
answer[0] = 2 × 3 × 4 = 24
answer[1] = 1 × 3 × 4 = 12
answer[2] = 1 × 2 × 4 = 8
answer[3] = 1 × 2 × 3 = 6
二、暴力做法为什么不合适
如果不考虑题目的时间要求,可以为每个位置重新遍历整个数组,跳过当前位置,将其他元素全部相乘。
数组长度为 n 时,一共有 n 个位置;每个位置又要遍历接近 n 个元素,因此时间复杂度是:
O(n²)
这种写法不难,但重复计算很多。例如,在计算 answer[2] 和 answer[3] 时,前面的 1 × 2 被反复计算了。题目要求 O(n),实际上就是在提醒我们:这些重复的乘积应该提前保存下来。
三、关键转化:把答案拆成左右两部分
仍然以数组 [1,2,3,4] 为例。
如果要计算下标 2 对应的答案,也就是排除元素 3,剩余元素可以分为:
左侧:[1,2]
右侧:[4]
所以:
answer[2] = (1 × 2) × 4 = 8
这时可以引入两个数组:
-
pre[i]:nums[i]左侧所有元素的乘积; -
suf[i]:nums[i]右侧所有元素的乘积。
那么每个位置的答案就是:
res[i] = pre[i] * suf[i];
注意,pre[i] 和 suf[i] 都不包含 nums[i] 本身。这一点是整个定义中最重要的地方。
四、前缀积是怎样计算的
前缀积从左向右计算:
pre[0] = 1;
for (int i = 1; i < n; i++) {
pre[i] = pre[i - 1] * nums[i - 1];
}
为什么公式中乘的是 nums[i - 1],而不是 nums[i]?
因为 pre[i] 只保存下标 i 左侧元素的乘积,不能包含当前位置。pre[i - 1] 已经包含了 i - 1 左侧的全部元素,再乘上 nums[i - 1],刚好得到 i 左侧所有元素的乘积。
对于 [1,2,3,4]:
pre[0] = 1
pre[1] = pre[0] × nums[0] = 1 × 1 = 1
pre[2] = pre[1] × nums[1] = 1 × 2 = 2
pre[3] = pre[2] × nums[2] = 2 × 3 = 6
所以:
pre = [1,1,2,6]
五、后缀积是怎样计算的
后缀积与前缀积完全对称,只不过需要从右向左计算:
suf[n - 1] = 1;
for (int i = n - 2; i >= 0; i--) {
suf[i] = suf[i + 1] * nums[i + 1];
}
suf[i] 只包含下标 i 右侧的元素,所以使用的是 nums[i + 1]。
对于 [1,2,3,4]:
suf[3] = 1
suf[2] = suf[3] × nums[3] = 1 × 4 = 4
suf[1] = suf[2] × nums[2] = 4 × 3 = 12
suf[0] = suf[1] × nums[1] = 12 × 2 = 24
最终得到:
suf = [24,12,4,1]
六、为什么两端要初始化为 1
这段代码里最容易让人疑惑的是:
pre[0] = 1;
suf[n - 1] = 1;
nums[0] 的左侧没有任何元素,nums[n - 1] 的右侧也没有任何元素。这里不能初始化为 0,因为任何数乘以 0 都会变成 0,后续结果会全部受到影响。
乘法中的单位元是 1:
x × 1 = x
因此,可以把"空的一侧"的乘积理解为 1。这样既符合乘法规则,也让首尾位置可以使用与其他位置完全相同的计算公式。
七、完整演算
把三个数组放在一起看,逻辑会更直观:
下标 i |
nums[i] |
左侧乘积 pre[i] |
右侧乘积 suf[i] |
res[i] |
|---|---|---|---|---|
| 0 | 1 | 1 | 24 | 24 |
| 1 | 2 | 1 | 12 | 12 |
| 2 | 3 | 2 | 4 | 8 |
| 3 | 4 | 6 | 1 | 6 |
最终:
res = [24,12,8,6]
整个过程没有使用除法,也没有为每个位置重新遍历数组。
八、Java 完整代码
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] pre = new int[n];
pre[0] = 1;
// 计算前缀积:pre[i] 表示 nums[i] 左侧所有元素的乘积
for (int i = 1; i < n; i++) {
pre[i] = pre[i - 1] * nums[i - 1];
}
int[] suf = new int[n];
suf[n - 1] = 1;
// 计算后缀积:suf[i] 表示 nums[i] 右侧所有元素的乘积
for (int i = n - 2; i >= 0; i--) {
suf[i] = suf[i + 1] * nums[i + 1];
}
int[] res = new int[n];
for (int i = 0; i < n; i++) {
res[i] = pre[i] * suf[i];
}
return res;
}
}
十一、复杂度分析
代码虽然进行了三次循环,但每次最多遍历数组一次,因此总时间复杂度仍然是:
O(n)
pre 和 suf 都是长度为 n 的辅助数组,因此额外空间复杂度为:
O(n)
输出数组 res 通常不计入额外空间,但两个辅助数组仍然需要计入。