除自身以外的乘积:左前缀乘右前缀,不用除法
answer[i] = 左边所有数的积 × 右边所有数的积。输出数组先装满左积,倒着再乘右积,额外空间 O(1)。
ans[0]=1,从左扫 ans[i]=ans[i−1]*nums[i−1],得到每个位置的左积。再令 right=1,从右往左 ans[i]*=right,然后 right*=nums[i]。时间 O(n),除答案数组外额外空间 O(1)。
这是 LeetCode 238. Product of Array Except Self。给整数数组 nums,返回 answer,使 answer[i] 等于 nums 里除了 nums[i] 以外所有数的乘积。题目要求 O(n) 且不能用除法。输出数组不计入额外空间。
主例 nums = [1,2,3,4]。除自己外:2×3×4=24,1×3×4=12,1×2×4=8,1×2×3=6。答案 [24,12,8,6]。演示先给出左积 [1,1,2,6],再乘完右积。
第一直觉是总乘积除以 nums[i]。数组里有 0 会除零;有两个 0 时多数位置应是 0,除法还要分类讨论。题目直接禁止除法。缺的是把「别人」拆成左段和右段。下面从这块缺口做两次扫描。
左边乘完,再倒着乘右边
下标 i 的「除自己」就是 nums[0..i−1] 的积乘上 nums[i+1..n−1] 的积。两端各自是前缀(后缀)积,可以线性预处理。开两个数组 left、right 能做对,但题目希望额外空间常数。
缺的是一个可以覆盖的工作区。输出数组 ans 正好能先承担 left:ans[i] 写成 i 左侧全乘。右侧用一个滚动变量 right 从 1 起,倒序扫时先 ans[i]*=right,再把 nums[i] 乘进 right。这样 right 永远表示「已经走过的更右侧」的积,不必再开数组。
从缺口写两趟。第一趟 ans[0]=1,因为 0 的左边是空,空积是 1。i 从 1 到 n−1,ans[i]=ans[i−1]*nums[i−1]。第二趟 right=1,i 从 n−1 降到 0:ans[i]*=right,right*=nums[i]。不要先更新 right 再乘,否则会把自己乘进去。
用手走主例。左扫:ans[0]=1;ans[1]=1×1=1;ans[2]=1×2=2;ans[3]=2×3=6。演示第一帧 left=[1,1,2,6]。右扫:i=3,ans[3]×=1 仍是 6,right 变成 4;i=2,2×4=8,right 变成 12;i=1,1×12=12,right 变成 24;i=0,1×24=24。演示第二帧 result=[24,12,8,6]。
空积是 1,不是 0:两端初始化必须是 1。出现一个 0 时,只有那个位置的答案是其余数之积,别的位置都是 0,左右前缀会自动做对。两个 0 则全体是 0。不要用除法「再特判 0」绕开题面。
先乘 ans,再更新 rightright 的含义是「i 右侧已经扫过的积」。乘进 ans[i] 之后,当前 nums[i] 才变成更左边格子的右侧。顺序反了,主例 ans[3] 会变成 24。
Go:两次扫描
func productExceptSelf(nums []int) []int {n := len(nums)ans := make([]int, n)ans[0] = 1for i := 1; i < n; i++ {ans[i] = ans[i-1] * nums[i-1]}right := 1for i := n - 1; i >= 0; i-- {ans[i] *= rightright *= nums[i]}return ans}
1ans[i] 第一趟只含左边。主例扫完是 [1,1,2,6]。空积用 1 垫 ans[0]。
2right 从 1 起倒着走。先 ans[i]*=right,再 right*=nums[i]。
3主例从右到左依次把 1、4、12、24 乘进去,得到 [24,12,8,6]。
总结
左积写入 ans,倒着再乘右积。主例 [1,2,3,4] → [24,12,8,6]。
- 总积相除碰到 0 会炸,也被题目禁止。拆成左右两段前缀。
- 空积是 1。两端初始化写 0,整段答案全是 0。
- 必须先乘进 ans 再更新 right,否则会乘上自己。