题目描述
给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
题目数据保证数组 nums 之中任意元素的全部前缀元素和后缀元素的乘积都在 32 位整数范围内。
请勿使用除法,且在 时间复杂度内完成此题。
输入
整数数组 nums
输出
数组 answer,其中 answer[i] 等于除 nums[i] 外其余元素的乘积
约束条件
2 <= nums.length <= 10^5-30 <= nums[i] <= 30- 保证数组
nums之中任意元素的全部前缀元素和后缀元素的乘积都在 32 位整数范围内
进阶约束
你可以在 的额外空间复杂度内完成这个题目吗?(出于对空间复杂度分析的目的,输出数组不被视为额外空间。)
示例1
输入:nums = [1,2,3,4]输出:[24,12,8,6]示例2
输入:nums = [-1,1,0,-3,3]输出:[0,0,9,0,0]解题思路
题目要求计算「除自己以外所有元素的乘积」——直观来想,对于位置 i,我们需要知道左边所有元素的乘积和右边所有元素的乘积,然后把它们乘起来。
💡 核心直觉:
answer[i] = (nums[0] × ... × nums[i-1]) × (nums[i+1] × ... × nums[n-1]),即「前缀积 × 后缀积」。如果我们能高效地获取每个位置的左乘积和右乘积,问题就迎刃而解了。
从暴力枚举出发
最直接的做法:对于每个位置 i,遍历整个数组计算乘积,跳过自身:
class Solution: def productExceptSelf(self, nums: List[int]) -> List[int]: n = len(nums) ans = [1] * n
for i in range(n): for j in range(n): if i != j: ans[i] *= nums[j]
return ans瓶颈分析:时间复杂度 ,当 时, 次操作远超时间限制。而且内部循环做了大量重复计算——对于相邻的位置 i 和 i+1,它们共享了大量乘数,但暴力法每次都从头乘起。
优化方向:能否复用已经算过的乘积?如果我们提前算好每个位置的「左边所有数的乘积」和「右边所有数的乘积」,那么每个位置的结果就是两者相乘,只需 即可得到。
左右前缀积数组
正如上文分析,我们可以用两个数组分别记录前缀积和后缀积:
left[i]:nums[0]到nums[i-1]的乘积(不含nums[i])right[i]:nums[i+1]到nums[n-1]的乘积(不含nums[i])
那么 answer[i] = left[i] * right[i]。
class Solution: def productExceptSelf(self, nums: List[int]) -> List[int]: n = len(nums) left = [1] * n right = [1] * n ans = [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]
# 合并 for i in range(n): ans[i] = left[i] * right[i]
return ans- 时间复杂度:,三次遍历
- 空间复杂度:,需要
left和right两个额外数组
虽然时间已经最优,但题目进阶要求提出:能否只用 额外空间?
空间优化:用输出数组替代前缀数组
观察上述代码,left 和 ans 计算时机可以合并——我们先用 ans 数组存储前缀积,然后在从右向左遍历时,用一个变量 r 跟踪后缀积,边乘边更新:
class Solution: def productExceptSelf(self, nums: List[int]) -> List[int]: n = len(nums) ans = [1] * n
# 第一遍:ans[i] 存储 i 左边所有元素的乘积 for i in range(1, n): ans[i] = ans[i - 1] * nums[i - 1]
# 第二遍:从右向左,用 r 跟踪右边乘积,直接乘入 ans r = 1 for i in range(n - 1, -1, -1): ans[i] *= r r *= nums[i]
return ans💡 关键优化:
ans数组一开始存放前缀积,然后从右向左遍历时「就地」乘上后缀积,一举两得。由于输出数组不计入额外空间,我们实现了 额外空间。
代码优化:双指针一次遍历
上面的解法需要两次独立的遍历。有没有办法在一次遍历中同时完成前后缀积的累积?
答案是可以的。我们维护两个变量 l 和 r,分别记录从左到右和从右到左的累积乘积。在每次迭代中,l 代表当前位置 i 的「左边乘积」,r 代表对称位置 n-i-1 的「右边乘积」——两边同时推进,互不干扰:
class Solution: def productExceptSelf(self, nums: List[int]) -> List[int]: n = len(nums) l = r = 1 res = [1] * n for i, x in enumerate(nums): res[i] *= l res[n - i - 1] *= r l *= x r *= nums[n - i - 1] return res逐行解读这段代码的执行逻辑:
- 初始化:
l = r = 1,分别代表「从左到右」和「从右到左」的累积乘积。res全为1作为初始值。 - 循环体内:
res[i] *= l:将i位置乘上它左边所有元素的累积积res[n - i - 1] *= r:将对称位置n-i-1乘上它右边所有元素的累积积l *= x:把当前元素x纳入左边的累积r *= nums[n - i - 1]:把对称位置的元素纳入右边的累积
- 循环结束时,
res[i]恰好被乘上了「左边累积 × 右边累积」,即除自身之外的乘积。
🧠 直觉理解:想象你从数组两端同时出发。
l是你从左边一路捡起的元素的乘积,每到一个新位置,先把这个位置的结果乘上l(这是「我左边的东西」),然后再把当前元素「装进」l给后面的位置用。r从右边做同样的事。两个方向在一次循环中交错推进,互不干扰,因为它们操作的是数组的两个不同位置。
执行过程可视化
以 nums = [1, 2, 3, 4] 为例,追踪 l、r 和 res 的变化:
i | x | n-i-1 | 操作前 res | l (操作前) | r (操作前) | 操作后 res | l (操作后) | r (操作后) |
|---|---|---|---|---|---|---|---|---|
| — | — | — | [1, 1, 1, 1] | 1 | 1 | — | — | — |
| 0 | 1 | 3 | [1, 1, 1, 1] | 1 | 1 | [1, 1, 1, 1] | 1 | 4 |
| 1 | 2 | 2 | [1, 1, 1, 1] | 1 | 4 | [1, 1, 4, 1] | 2 | 12 |
| 2 | 3 | 1 | [1, 1, 4, 1] | 2 | 12 | [1, 12, 8, 1] | 6 | 24 |
| 3 | 4 | 0 | [1, 12, 8, 1] | 6 | 24 | [24, 12, 8, 6] | 24 | 24 |
最终 res = [24, 12, 8, 6],正是答案。
⚠️ 注意:
l在当前位置被用来更新res[i]之后才乘上x。这个顺序很关键——确保l始终代表「不含当前元素的左边乘积」。r同理。
为什么这种写法是对的?
对于位置 i,在循环的某一轮中:
- 当
i作为「左指针」被处理时,l包含了nums[0..i-1]的乘积,所以res[i]被正确地乘上了左边部分 - 当
n-i-1等于i(即左右指针相遇),r包含了nums[i+1..n-1]的乘积,所以res[i]被正确地乘上了右边部分
因此,当循环结束时,每个 res[i] 都恰好被乘上了它左边和右边的所有元素——不多不少。
复杂度
- 时间复杂度:,只需一次遍历。
- 空间复杂度:(输出数组不计入额外空间),仅使用了
l、r两个变量。
时间已是最优——因为输出结果需要为 个位置分别计算,至少需要遍历一次数组。
代码优化
三种写法的对比
| 对比项 | 左右数组法 | 两次遍历法 | 单次双指针法 |
|---|---|---|---|
| 遍历次数 | 3 次 | 2 次 | 1 次 |
| 额外空间 | |||
| 代码行数 | ~12 行 | ~11 行 | ~9 行 |
| 可读性 | ⭐⭐⭐⭐⭐(最直观) | ⭐⭐⭐⭐ | ⭐⭐⭐(需理解双指针交错逻辑) |
| 推荐场景 | 面试中先展示思路 | 面试中优化后的标准解 | 追求极简和 Pythonic |
三种写法的时间复杂度均为 ,差异在于常数因子和代码风格。面试中建议从「左右数组法」讲起,展示清晰的思维递进过程,最后落地到「两次遍历法」或「单次双指针法」。
💡 面试建议:面试官更看重你的思维过程而非最终代码。先说「我们需要前缀积和后缀积」,用左右数组法写出第一版,再逐步优化——这个过程本身就能展现你的问题分解和优化能力。
处理零值的情况
nums 中可能包含 0(如示例2 [-1, 1, 0, -3, 3]),但上述算法天然能正确处理——因为前缀积会正常累积 0,后缀积同理。唯一需要注意的是:如果题目不保证乘积在 32 位范围内(本题保证了),可能需要考虑溢出问题。
总结
「除自身以外数组的乘积」的核心在于将问题拆解为两个独立维度——前缀积和后缀积——然后合并。这个思维模式在数组问题中非常经典:
- 前缀和 / 前缀积:预处理每个位置「从开头到这里的累积信息」
- 后缀和 / 后缀积:预处理每个位置「从这里到末尾的累积信息」
- 合并:在每个位置组合前后信息得到答案
🧠 核心收获:当你遇到「除了某个元素之外的所有元素」这种问题时,第一反应就应该是——把这个元素从中间”挖掉”,然后分别看它的左边和右边。前缀 + 后缀的组合模式能帮你把看似需要 的问题降到 。
解决本题后,推荐挑战以下延伸题目——它们都涉及「前缀 + 后缀」或「预处理 + 合并」的思想:
- 42. 接雨水:每个位置能接多少水取决于它左边的最大高度和右边的最大高度——正是前缀最大值 + 后缀最大值的组合
- 135. 分发糖果:每个孩子分多少糖取决于他左边和右边的相对排名——两次遍历,分别满足左右约束
- 152. 乘积最大子数组:将前缀积的思想延伸到子数组问题,乘积的符号翻转带来了新的挑战
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









