LeetCode 238:除自身以外数组的乘积,从暴力枚举到前后缀乘积
前言
刷数组题时,经常会遇到这样一种场景:
对于数组中的每一个位置,需要统计它左边和右边所有元素的信息。
例如:
- 除自身以外数组的乘积
- 接雨水
- 分糖果
- 股票问题
- 前缀和问题
LeetCode 238《除自身以外数组的乘积》就是其中最经典的一道题。
这道题最大的价值并不在于代码本身,而在于它体现了一种非常重要的优化思想:
用预计算换时间,避免重复计算。
一、题目描述
给定一个整数数组 nums。
返回一个数组 answer,使得:
python
answer[i]
=
nums 中除 nums[i] 之外所有元素的乘积
示例:
python
nums = [1,2,3,4]
输出:
[24,12,8,6]
解释:
python
answer[0] = 2×3×4 = 24
answer[1] = 1×3×4 = 12
answer[2] = 1×2×4 = 8
answer[3] = 1×2×3 = 6
并且题目还有两个限制:
- 不允许使用除法
- 时间复杂度必须为 O(n)
二、最容易想到的方法
对于每一个位置:
重新遍历整个数组。
跳过自己。
把其余元素全部乘起来。
例如:
python
nums = [1,2,3,4]
计算 answer2:
python
1 × 2 × 4
计算 answer3:
python
1 × 2 × 3
...
代码类似:
python
for i in range(n):
product = 1
for j in range(n):
if i != j:
product *= nums[j]
问题在哪?
很多乘法被重复计算了。
例如:
python
1 × 2
被算了很多次。
python
2 × 3
也被算了很多次。
时间复杂度:
text
O(n²)
题目要求 O(n)。
因此必须优化。
三、关键突破口
先看一个位置:
python
nums = [1,2,3,4]
计算:
python
answer[2]
实际上就是:
python
1 × 2 × 4
可以拆开:
python
(1 × 2) × (4)
也就是:
python
左边所有元素乘积
×
右边所有元素乘积
于是得到核心结论:
python
answer[i]
=
左侧乘积
×
右侧乘积
问题瞬间变简单了。
四、前缀积与后缀积
对于数组:
python
nums = [1,2,3,4]
前缀积(左侧乘积)
定义:
python
left[i]
=
i左边所有元素的乘积
得到:
python
left = [1,1,2,6]
为什么?
python
left[0] = 1
left[1] = 1
left[2] = 1×2 = 2
left[3] = 1×2×3 = 6
后缀积(右侧乘积)
定义:
python
right[i]
=
i右边所有元素的乘积
得到:
python
right = [24,12,4,1]
为什么?
python
right[3] = 1
right[2] = 4
right[1] = 3×4 = 12
right[0] = 2×3×4 = 24
五、答案如何得到?
直接相乘即可:
python
answer[i]
=
left[i] * right[i]
例如:
python
answer[2]
=
2 × 4
=
8
结果:
python
[24,12,8,6]
完全正确。
六、双辅助数组解法
第一步:计算左侧乘积
递推公式:
python
left[i]
=
left[i-1] * nums[i-1]
为什么乘 i-1?
因为题目要求:
python
不包含自己
第二步:计算右侧乘积
递推公式:
python
right[i]
=
right[i+1] * nums[i+1]
同样不能包含自己。
第三步:组合答案
python
answer[i]
=
left[i] * right[i]
完整代码
python
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
left = [1] * n
right = [1] * n
for i in range(1, n):
left[i] = left[i-1] * nums[i-1]
for i in range(n-2, -1, -1):
right[i] = right[i+1] * nums[i+1]
answer = [0] * n
for i in range(n):
answer[i] = left[i] * right[i]
return answer
七、空间优化版
面试官通常会追问:
能不能不使用两个辅助数组?
观察:
最终答案只需要:
python
left[i]
和
python
right[i]
相乘。
其实没必要把 right 数组全部存下来。
优化思路
第一遍:
直接把左侧乘积存进答案数组。
例如:
python
answer = [1,1,2,6]
第二遍从右往左遍历。
用一个变量:
python
right_product
实时维护右侧乘积。
每走一步:
python
answer[i]
=
answer[i] * right_product
然后更新:
python
right_product *= nums[i]
即可。
八、空间优化版代码
python
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
answer = [1] * n
for i in range(1, n):
answer[i] = answer[i-1] * nums[i-1]
right_product = 1
for i in range(n-1, -1, -1):
answer[i] *= right_product
right_product *= nums[i]
return answer
九、为什么额外空间是 O(1)?
很多同学会疑惑:
python
answer 不也是数组吗?
为什么还能说 O(1)?
原因是:
题目要求返回结果数组。
这个数组无论如何都必须存在。
算法分析时:
text
结果数组不计入额外空间
因此:
text
额外使用的空间
=
right_product 一个变量
=
O(1)
十、高频易错点
1. 把当前元素算进去
错误:
python
left[i]
=
left[i-1] * nums[i]
这样会把自己乘进去。
正确:
python
left[i]
=
left[i-1] * nums[i-1]
2. 右侧乘积遍历方向错误
必须:
python
从右往左
因为当前位置依赖右边结果。
3. 边界初始化错误
正确:
python
left[0] = 1
right[n-1] = 1
因为:
python
空乘积 = 1
不是 0。
4. 倒序 range 写错
正确:
python
range(n-2, -1, -1)
如果写:
python
range(n-2, 0, -1)
索引0不会被遍历到。
总结
这道题真正考察的不是乘法,而是:
如何通过预计算消除重复计算。
核心公式只有一句:
python
answer[i]
=
左侧乘积 × 右侧乘积
利用前缀积和后缀积,我们把原本 O(n²) 的暴力枚举优化到了 O(n)。
这也是很多数组题、动态规划题和区间统计题中经常出现的经典思想,值得重点掌握。