除自身以外数组的乘积 图解题解
不能用除法,还要求每个位置的「其余乘积」?前后缀各扫一遍,O(1) 额外空间解决。
想象你站在一排数字中间要算「除我以外所有数之积」。第一遍从左往右走:把你左侧所有数的累积积记进结果数组;第二遍从右往左,用一个滚动变量 right 追踪右侧累积积,每到一个位置把 right 乘进结果数组再更新 right。两遍扫完,除法没用过,额外变量只用了一个 right,输出数组之外零开销。
这道题到底在问什么
- 输入
- nums = [1,2,3,4]
- 输出
- [24,12,8,6]
最优解:为什么这么做
一句话答案:LeetCode 238 除自身以外数组的乘积在禁用除法的前提下,标准解是前后缀乘积两遍扫描:answer[i] 恰好等于 i 左边所有数的积乘以右边所有数的积,第一遍从左到右把左前缀积写进答案数组,第二遍从右到左用一个变量滚动右后缀积再乘上去,时间 O(n)、额外空间 O(1)。
这道题真正在问什么
给数组 nums,返回同长数组 answer,answer[i] 等于除 nums[i] 之外所有元素的乘积,比如 [1,2,3,4] 的答案是 [24,12,8,6]。题目附加两条硬约束:不许用除法、时间必须 O(n)。这两条约束就是解法的路标——它们把最省事的那条路堵死,逼你去找乘法本身的结构。
为什么全积除以自己走不通
最诱人的做法是先算全体乘积 total,再让每个位置等于 total / nums[i],一遍就完。但题目明说禁止除法;就算允许,这条路也有真实的坑:数组里只要有一个 0,total 就是 0,所有非零位置都会被算成 0;有两个以上 0 时更是全盘皆 0,必须层层特判。除法方案的脆弱反过来提示我们:得直接「凑」出其余元素的积,而不是「除」出来。
关键观察:答案等于左积乘右积
位置 i 除自身以外的元素,天然分成两段:i 左边的所有数、i 右边的所有数。所以 answer[i] 就等于左边一段的积乘以右边一段的积。而「左边一段的积」正是经典的前缀积,从左到右用一个变量一路累乘就能递推出来;「右边一段的积」对称地是后缀积,从右到左递推。两个方向各扫一遍,每个位置的两块拼图就都齐了。边界也很自然:最左位置左边没有数,约定空积为 1,最右位置同理。
为什么两遍扫描只要 O(1) 额外空间
直接开两个数组分别存前缀积和后缀积也能对,但要 O(n) 额外空间。省法是把答案数组本身当工作区:第一遍从左到右维护变量 prefix,先把 prefix 写进 answer[i],再让 prefix 乘上 nums[i]——「先写后乘」的顺序保证写进去的恰好是不含自己的左积。
第二遍从右到左,用变量 suffix 同样先乘进 answer[i]、再累乘 nums[i]。两遍的不变量一致:走到位置 i 时,prefix(或 suffix)恰好等于 i 左边(或右边)全部元素的积。第二遍方向绝不能又从左往右——方向反了,suffix 积累的就不是右边的数,结果整体错位。
复杂度与容易踩的坑
时间 O(n):两遍线性扫描,总共约 2n 次乘法。额外空间 O(1):按惯例输出数组不计入,全程只多用 prefix、suffix 两个标量。最常见的三个错:偷偷用除法(数组含 0 就炸)、第二遍方向写反、以及把「先写答案再累乘」的顺序颠倒,把当前位置自己也乘了进去。
▶ 动画逐步走查(共 26 步)——想跟着动画一帧帧对照就展开
- 3记住这条「答案 = 左边的积 × 右边的积;两遍扫:先存左前缀积,再乘右后缀积,不用除法」,下面每帧都在套它。
- 4第一遍从左往右,处理位置 0。此刻 prefix=1,它正是「0 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[0]。
- 5把 prefix=1 落进 answer[0](这是它的左前缀积)。然后把 nums[0]=2 乘进 prefix,得到 2,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
- 6第一遍从左往右,处理位置 1。此刻 prefix=2,它正是「1 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[1]。
- 7把 prefix=2 落进 answer[1](这是它的左前缀积)。然后把 nums[1]=3 乘进 prefix,得到 6,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
- 8第一遍从左往右,处理位置 2。此刻 prefix=6,它正是「2 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[2]。
- 9把 prefix=6 落进 answer[2](这是它的左前缀积)。然后把 nums[2]=4 乘进 prefix,得到 24,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
- 10第一遍从左往右,处理位置 3。此刻 prefix=24,它正是「3 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[3]。
- 11把 prefix=24 落进 answer[3](这是它的左前缀积)。然后把 nums[3]=5 乘进 prefix,得到 120,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
- 12第一遍从左往右,处理位置 4。此刻 prefix=120,它正是「4 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[4]。
- 13把 prefix=120 落进 answer[4](这是它的左前缀积)。然后把 nums[4]=6 乘进 prefix,得到 720,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
- 14第一遍从左往右,处理位置 5。此刻 prefix=720,它正是「5 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[5]。
- 15把 prefix=720 落进 answer[5](这是它的左前缀积)。然后把 nums[5]=7 乘进 prefix,得到 5040,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
- 16第二遍从右往左,处理位置 5。此刻 suffix=1,是「5 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[5] 现在还只是左前缀积 720,下一帧给它乘上右后缀积就补齐了。
- 17answer[5] = 左前缀积 720 × 右后缀积 1 = 720,这就是位置 5 的最终答案。再把 nums[5]=7 乘进 suffix(得 7)留给左边。
- 18第二遍从右往左,处理位置 4。此刻 suffix=7,是「4 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[4] 现在还只是左前缀积 120,下一帧给它乘上右后缀积就补齐了。
- 19answer[4] = 左前缀积 120 × 右后缀积 7 = 840,这就是位置 4 的最终答案。再把 nums[4]=6 乘进 suffix(得 42)留给左边。
- 20第二遍从右往左,处理位置 3。此刻 suffix=42,是「3 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[3] 现在还只是左前缀积 24,下一帧给它乘上右后缀积就补齐了。
- 21answer[3] = 左前缀积 24 × 右后缀积 42 = 1008,这就是位置 3 的最终答案。再把 nums[3]=5 乘进 suffix(得 210)留给左边。
- 22第二遍从右往左,处理位置 2。此刻 suffix=210,是「2 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[2] 现在还只是左前缀积 6,下一帧给它乘上右后缀积就补齐了。
- 23answer[2] = 左前缀积 6 × 右后缀积 210 = 1260,这就是位置 2 的最终答案。再把 nums[2]=4 乘进 suffix(得 840)留给左边。
- 24第二遍从右往左,处理位置 1。此刻 suffix=840,是「1 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[1] 现在还只是左前缀积 2,下一帧给它乘上右后缀积就补齐了。
- 25answer[1] = 左前缀积 2 × 右后缀积 840 = 1680,这就是位置 1 的最终答案。再把 nums[1]=3 乘进 suffix(得 2520)留给左边。
- 26第二遍从右往左,处理位置 0。此刻 suffix=2520,是「0 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[0] 现在还只是左前缀积 1,下一帧给它乘上右后缀积就补齐了。
- 27answer[0] = 左前缀积 1 × 右后缀积 2520 = 2520,这就是位置 0 的最终答案。再把 nums[0]=2 乘进 suffix(得 5040)留给左边。
- 28两遍扫完,每个位置都等于「左边所有数的积 × 右边所有数的积」:[2520, 1680, 1260, 1008, 840, 720]。全程没用一次除法,只用了 prefix、suffix 两个变量,时间 O(n)、额外空间 O(1)。
⚠️ 容易写错的地方
✗ 错:用「全积 ÷ nums[i]」
✓ 对:题目禁止除法
且当 nums 里有 0 时除法会除零出错
✗ 错:开两个数组分别存前缀积、后缀积
✓ 对:可以但额外空间 O(n)
把答案就地写在 answer 里、用标量滚动即可降到 O(1)
✗ 错:第二遍方向写反(又从左到右)
✓ 对:必须从右往左
suffix 要积累的是「右边」的数,方向反了就乘错了
完整代码(Python / C++ / Java)
Python
def productExceptSelf(nums):
n = len(nums)
answer = [1] * n
prefix = 1 # 左边所有数的积
for i in range(n): # 第一遍:从左到右
answer[i] = prefix # 先存左前缀积
prefix *= nums[i]
suffix = 1 # 右边所有数的积
for i in range(n - 1, -1, -1): # 第二遍:从右到左
answer[i] *= suffix # 再乘右后缀积
suffix *= nums[i]
return answerC++
vector<int> productExceptSelf(vector<int>& nums){
int n = nums.size();
vector<int> answer(n, 1);
long prefix = 1; // 左边所有数的积
for(int i = 0; i < n; i++){ // 第一遍:从左到右
answer[i] = prefix; // 先存左前缀积
prefix *= nums[i];
}
long suffix = 1; // 右边所有数的积
for(int i = n - 1; i >= 0; i--){ // 第二遍:从右到左
answer[i] *= suffix; // 再乘右后缀积
suffix *= nums[i];
}
return answer;
}Java
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] answer = new int[n];
int prefix = 1; // 左边所有数的积
for (int i = 0; i < n; i++) { // 第一遍:从左到右
answer[i] = prefix; // 先存左前缀积
prefix *= nums[i];
}
int suffix = 1; // 右边所有数的积
for (int i = n - 1; i >= 0; i--) { // 第二遍:从右到左
answer[i] *= suffix; // 再乘右后缀积
suffix *= nums[i];
}
return answer;
}复杂度
时间
O(n)
两遍线性扫描,2n 次乘法
空间
O(1)
只用 prefix/suffix 两个标量;输出数组不计入额外空间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 除自身以外数组的乘积 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如果允许用除法,怎么做?有什么风险?+
先求全体乘积 total,再 answer[i] = total / nums[i],O(n)。但题目禁止除法;且当数组含一个 0 时,total=0 会让所有非零位置算错,含两个及以上 0 时全是 0——必须特判,远不如前后缀积干净。
能不能只用 O(1) 额外空间(不算输出数组)?+
能。第一遍把左前缀积直接写进输出数组 answer,第二遍用一个标量 suffix 从右往左把右后缀积乘上去,全程只多用 prefix、suffix 两个变量。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 除自身以外数组的乘积 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。