这道题的约束很"刁钻":O(n) 时间、不用除法、最好 O(1) 额外空间。三个条件同时满足,直接把"先算总乘积再除"和"前缀积数组"的方案都堵死了。但换个角度想------answeri 就是"左边的乘积 × 右边的乘积",分两遍扫就行了。
题目长什么样
给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除了 nums[i] 之外其余各元素的乘积。不要使用除法,O(n) 时间复杂度。
输入 :nums = [1,2,3,4]
输出 :[24,12,8,6]
说人话就是:对于每个位置,算其他所有位置的乘积。
第一反应:先算总乘积再除
text
总乘积 = 1 × 2 × 3 × 4 = 24
answer[0] = 24 / 1 = 24
answer[1] = 24 / 2 = 12
answer[2] = 24 / 3 = 8
answer[3] = 24 / 4 = 6
但题目明确说"不要使用除法"------因为有 0 的情况。如果数组中有 0,总乘积就是 0,除法全部失效。所以这个方案直接淘汰。
第二反应:前缀积 + 后缀积
answer[i] 可以拆成两部分:
text
answer[i] = nums[0] × ... × nums[i-1] × nums[i+1] × ... × nums[n-1]
↑ 左边的乘积 (prefix) ↑ 右边的乘积 (suffix)
如果能预先算出每个位置的"左乘积"和"右乘积",一乘就得到答案。
直观版本:两个额外数组
python
class SolutionExtra:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
prefix = [1] * n
suffix = [1] * n
for i in range(1, n):
prefix[i] = prefix[i - 1] * nums[i - 1]
for i in range(n - 2, -1, -1):
suffix[i] = suffix[i + 1] * nums[i + 1]
return [prefix[i] * suffix[i] for i in range(n)]
跑一遍示例 1:
text
nums = [1, 2, 3, 4]
prefix: [1, 1, 2, 6] ← prefix[i] = nums[0..i-1] 的乘积
suffix: [24, 12, 4, 1] ← suffix[i] = nums[i+1..n-1] 的乘积
answer: [1×24, 1×12, 2×4, 6×1] = [24, 12, 8, 6] ✓
| 维度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n) | 三次遍历 |
| 空间 | O(n) | prefix + suffix 两个数组 |
时间满足要求了,但空间是 O(n)。进阶要求 O(1) 额外空间------能做到吗?
最优解:用 answer 数组本身当前缀积
关键观察:输出数组不算额外空间 。所以我们可以直接用 answer 数组存前缀积,然后用一个变量从右往左累乘后缀积。
python
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
answer = [1] * n
# 第一遍:从左到右,answer[i] = 左边所有元素的乘积
left = 1
for i in range(n):
answer[i] = left
left *= nums[i]
# 第二遍:从右到左,answer[i] *= 右边所有元素的乘积
right = 1
for i in range(n - 1, -1, -1):
answer[i] *= right
right *= nums[i]
return answer
跑一遍示例 1
text
nums = [1, 2, 3, 4]
第一遍(从左到右): answer[i] = 左乘积
i=0: answer[0]=1, left=1×1=1
i=1: answer[1]=1, left=1×2=2
i=2: answer[2]=2, left=2×3=6
i=3: answer[3]=6, left=6×4=24
answer = [1, 1, 2, 6]
第二遍(从右到左): answer[i] *= 右乘积
i=3: answer[3]=6×1=6, right=1×4=4
i=2: answer[2]=2×4=8, right=4×3=12
i=1: answer[1]=1×12=12, right=12×2=24
i=0: answer[0]=1×24=24, right=24×1=24
answer = [24, 12, 8, 6] ✓
跑一遍示例 2
text
nums = [-1, 1, 0, -3, 3]
第一遍(从左到右):
answer = [1, -1, -1, 0, 0]
第二遍(从右到左):
i=4: answer[4]=0×1=0, right=1×3=3
i=3: answer[3]=0×3=0, right=3×(-3)=-9
i=2: answer[2]=-1×(-9)=9, right=-9×0=0
i=1: answer[1]=-1×0=0, right=0×1=0
i=0: answer[0]=1×0=0, right=0×(-1)=0
answer = [0, 0, 9, 0, 0] ✓
| 维度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n) | 两遍扫描 |
| 空间 | O(1) | 只用了 left 和 right 两个变量(answer 不算) |
两种解法放在一起看
| 解法 | 时间 | 空间 | 思路 |
|---|---|---|---|
| 前缀积 + 后缀积数组 | O(n) | O(n) | 最直观,两个辅助数组 |
| answer 复用 + 单变量 | O(n) | O(1) | 最优解,两遍扫描 |
两种解法的核心思路完全一样------都是"左边乘积 × 右边乘积"。区别只在于存前缀积的方式:用额外数组还是复用 answer 数组。
这道题教会我什么
"拆成两部分"是解决乘积问题的万能思路
不允许用除法,那就把乘积拆成"左边"和"右边"。这个思路不依赖除法,而且天然 O(n)。类似的思想在树的问题里也很常见:对于某个节点,结果 = 左子树的信息 × 右子树的信息。
输出数组不算额外空间
这个约定在很多题目里都有。它意味着你可以把输出数组当工作空间使用------先存中间结果,再逐步更新成最终答案。这道题就是先存前缀积,再乘上后缀积。
两遍扫描的套路
text
第一遍从左到右:累积左边的信息
第二遍从右到左:累积右边的信息
这个"两遍扫描"的套路在很多题目里都能看到:接雨水(LeetCode 42)、每日温度(LeetCode 739)、字符串解码(LeetCode 394)。掌握了这个模式,一整类题都能搞定。
完整测试代码
python
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
answer = [1] * n
left = 1
for i in range(n):
answer[i] = left
left *= nums[i]
right = 1
for i in range(n - 1, -1, -1):
answer[i] *= right
right *= nums[i]
return answer
if __name__ == "__main__":
s = Solution()
nums = [1, 2, 3, 4]
print(f"输入: {nums}, 输出: {s.productExceptSelf(nums)}")
nums = [-1, 1, 0, -3, 3]
print(f"输入: {nums}, 输出: {s.productExceptSelf(nums)}")
nums = [2, 3, 0, 0]
print(f"输入: {nums}, 输出: {s.productExceptSelf(nums)}")
nums = [1, 2]
print(f"输入: {nums}, 输出: {s.productExceptSelf(nums)}")
nums = [4, 3, 2, 1, 2]
print(f"输入: {nums}, 输出: {s.productExceptSelf(nums)}")
相关题目推荐:
- LeetCode 42 · 接雨水(同样的左右两遍扫描套路)
- LeetCode 152 · 乘积最大子数组(同样是乘积问题,但求连续子数组)
- LeetCode 739 · 每日温度(从右往左扫描的思路)