算法238. 除自身以外数组的乘积
🧩 题目描述
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n) 时间复杂度内完成此题。
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)
✅ 解法:前后缀分解(Prefix and Suffix Product)
🔑 核心思想
将每个位置
i
的答案拆分为两部分:
i
左边所有元素的乘积 →pre[i]
i
右边所有元素的乘积 →suf[i]
则:
answer[i]=pre[i]×suf[i]\text{answer}[i] = \text{pre}[i] \times \text{suf}[i] answer[i]=pre[i]×suf[i]
这称为「前后缀分解」技巧,是处理“除自身外乘积”类问题的标准方法。
🧱 算法步骤
-
构造前缀乘积数组
pre
pre[i]
表示nums[0] × nums[1] × ... × nums[i-1]
- 即:从左到右,不包含
nums[i]
的乘积 - 初始值:
pre[0] = 1
(空乘积定义为 1)
-
构造后缀乘积数组
suf
suf[i]
表示nums[i+1] × nums[i+2] × ... × nums[n-1]
- 即:从右到左,不包含
nums[i]
的乘积 - 初始值:
suf[n-1] = 1
-
合并结果
answer[i] = pre[i] * suf[i]
💻 代码实现
class Solution:def productExceptSelf(self, nums: List[int]) -> List[int]:n = len(nums)# Step 1: 构造前缀乘积 pre[i] = nums[0] * ... * nums[i-1]pre = [1] # pre[0] = 1(左边没有元素)for i in range(1, n):pre.append(pre[i-1] * nums[i-1])# Step 2: 构造后缀乘积 suf[i] = nums[i+1] * ... * nums[n-1]suf = [1] * n # 初始化 suf 数组# 从右往左计算for i in range(n-2, -1, -1):suf[i] = suf[i+1] * nums[i+1]# Step 3: 合并前后缀return [p * s for p, s in zip(pre, suf)]
📊 示例演示
nums = [1, 2, 3, 4]
i | nums[i] | pre[i](左边乘积) | suf[i](右边乘积) | answer[i] = pre[i] × suf[i] |
---|---|---|---|---|
0 | 1 | 1 | 2×3×4 = 24 | 1 × 24 = 24 |
1 | 2 | 1 | 3×4 = 12 | 1 × 12 = 12 |
2 | 3 | 1×2 = 2 | 4 | 2 × 4 = 8 |
3 | 4 | 1×2×3 = 6 | 1 | 6 × 1 = 6 |
✅ 输出:[24, 12, 8, 6]
📌 关键细节说明
1. 为什么 pre[0] = 1
?
- 表示
nums[0]
左边没有元素,空乘积定义为 1(乘法单位元) - 类似地,
suf[n-1] = 1
2. 递推公式
pre[i] = pre[i-1] * nums[i-1]
suf[i] = suf[i+1] * nums[i+1]
3. 为什么不能用除法?
- 题目明确禁止
- 而且如果
nums
中有0
,除法会出错或需要特殊处理
⚡ 进阶优化:空间复杂度 O(1)(不计输出数组)
可以将 pre
直接作为答案数组,再用一个变量从右往左累乘后缀:
class Solution:def productExceptSelf(self, nums: List[int]) -> List[int]:n = len(nums)answer = [1] * n# Step 1: answer[i] 先存 pre[i]for i in range(1, n):answer[i] = answer[i-1] * nums[i-1]# Step 2: 用 right 表示后缀乘积,从右往左更新 answerright = 1for i in range(n-1, -1, -1):answer[i] *= rightright *= nums[i]return answer
- ✅ 时间复杂度:O(n)
- ✅ 空间复杂度:O(1)(不计输出数组)
🧠 方法本质:分治思想
把“整体乘积除以自身”转化为:
- 左边乘积 × 右边乘积
避免了对每个位置重新计算,实现高效递推。
📚 相似题目推荐
题目 | 链接 | 思路关联 |
---|---|---|
135. 分发糖果 | 左右两次遍历 | 类似前后缀思想 |
2104. 子数组范围和 | 前后缀 + 单调栈 | 范围最值乘积 |
1991. 找到数组的中间位置 | 前缀和 | 基础前缀思想 |
✅ 总结
技巧 | 说明 |
---|---|
前后缀分解 | 将复杂问题拆解为左右两部分独立计算 |
空乘积为 1 | 保证递推边界正确 |
避免除法 | 更通用,适用于含 0 的情况 |
可空间优化 | 用输出数组或单变量替代额外空间 |
📌 一句话记住:
“左积 × 右积 = 除自身外的总积”
—— 掌握前后缀分解,轻松应对“除自身外乘积”类问题!