题目描述
思路解析
一句话答案: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 就炸)、第二遍方向写反、以及把「先写答案再累乘」的顺序颠倒,把当前位置自己也乘了进去。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这条「答案 = 左边的积 × 右边的积;两遍扫:先存左前缀积,再乘右后缀积,不用除法」,下面每帧都在套它。
第一遍从左往右,处理位置 0。此刻 prefix=1,它正是「0 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[0]。
把 prefix=1 落进 answer[0](这是它的左前缀积)。然后把 nums[0]=2 乘进 prefix,得到 2,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
第一遍从左往右,处理位置 1。此刻 prefix=2,它正是「1 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[1]。
把 prefix=2 落进 answer[1](这是它的左前缀积)。然后把 nums[1]=3 乘进 prefix,得到 6,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
第一遍从左往右,处理位置 2。此刻 prefix=6,它正是「2 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[2]。
把 prefix=6 落进 answer[2](这是它的左前缀积)。然后把 nums[2]=4 乘进 prefix,得到 24,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
第一遍从左往右,处理位置 3。此刻 prefix=24,它正是「3 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[3]。
把 prefix=24 落进 answer[3](这是它的左前缀积)。然后把 nums[3]=5 乘进 prefix,得到 120,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
第一遍从左往右,处理位置 4。此刻 prefix=120,它正是「4 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[4]。
把 prefix=120 落进 answer[4](这是它的左前缀积)。然后把 nums[4]=6 乘进 prefix,得到 720,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
第一遍从左往右,处理位置 5。此刻 prefix=720,它正是「5 左边所有数」的乘积(位置 0 左边没有数,约定为 1)。下一帧就把它写进 answer[5]。
把 prefix=720 落进 answer[5](这是它的左前缀积)。然后把 nums[5]=7 乘进 prefix,得到 5040,供下一个位置使用。第一遍结束后,answer 里装的全是「各自的左前缀积」。
第二遍从右往左,处理位置 5。此刻 suffix=1,是「5 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[5] 现在还只是左前缀积 720,下一帧给它乘上右后缀积就补齐了。
answer[5] = 左前缀积 720 × 右后缀积 1 = 720,这就是位置 5 的最终答案。再把 nums[5]=7 乘进 suffix(得 7)留给左边。
第二遍从右往左,处理位置 4。此刻 suffix=7,是「4 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[4] 现在还只是左前缀积 120,下一帧给它乘上右后缀积就补齐了。
answer[4] = 左前缀积 120 × 右后缀积 7 = 840,这就是位置 4 的最终答案。再把 nums[4]=6 乘进 suffix(得 42)留给左边。
第二遍从右往左,处理位置 3。此刻 suffix=42,是「3 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[3] 现在还只是左前缀积 24,下一帧给它乘上右后缀积就补齐了。
answer[3] = 左前缀积 24 × 右后缀积 42 = 1008,这就是位置 3 的最终答案。再把 nums[3]=5 乘进 suffix(得 210)留给左边。
第二遍从右往左,处理位置 2。此刻 suffix=210,是「2 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[2] 现在还只是左前缀积 6,下一帧给它乘上右后缀积就补齐了。
answer[2] = 左前缀积 6 × 右后缀积 210 = 1260,这就是位置 2 的最终答案。再把 nums[2]=4 乘进 suffix(得 840)留给左边。
第二遍从右往左,处理位置 1。此刻 suffix=840,是「1 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[1] 现在还只是左前缀积 2,下一帧给它乘上右后缀积就补齐了。
answer[1] = 左前缀积 2 × 右后缀积 840 = 1680,这就是位置 1 的最终答案。再把 nums[1]=3 乘进 suffix(得 2520)留给左边。
第二遍从右往左,处理位置 0。此刻 suffix=2520,是「0 右边所有数」的乘积(最右位置右边没有数,约定为 1)。answer[0] 现在还只是左前缀积 1,下一帧给它乘上右后缀积就补齐了。
answer[0] = 左前缀积 1 × 右后缀积 2520 = 2520,这就是位置 0 的最终答案。再把 nums[0]=2 乘进 suffix(得 5040)留给左边。
两遍扫完,每个位置都等于「左边所有数的积 × 右边所有数的积」:[2520, 1680, 1260, 1008, 840, 720]。全程没用一次除法,只用了 prefix、suffix 两个变量,时间 O(n)、额外空间 O(1)。
边界先想清:数组含 0 时,除法解会除零崩溃,而前后缀积法天然正确。
两个高频追问:除法解的隐患(含 0)与 O(1) 空间的实现。
参考代码
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 answer复杂度
- 时间:O(n),两遍线性扫描,2n 次乘法
- 空间:O(1),只用 prefix/suffix 两个标量;输出数组不计入额外空间
易错点
面试追问把动画讲成自己的话
追问如果允许用除法,怎么做?有什么风险?
追问能不能只用 O(1) 额外空间(不算输出数组)?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
有效的数独
LeetCode 36 · 中等 · 沿着 数组 & 哈希 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题