mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
2273 字
6 分钟
除自身以外数组的乘积
2026-07-03

题目描述#

题目链接

给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。

题目数据保证数组 nums 之中任意元素的全部前缀元素和后缀元素的乘积都在 32 位整数范围内。

请勿使用除法,且在 O(n)O(n) 时间复杂度内完成此题。

输入#

整数数组 nums

输出#

数组 answer,其中 answer[i] 等于除 nums[i] 外其余元素的乘积

约束条件#

  • 2 <= nums.length <= 10^5
  • -30 <= nums[i] <= 30
  • 保证数组 nums 之中任意元素的全部前缀元素和后缀元素的乘积都在 32 位整数范围内

进阶约束#

你可以在 O(1)O(1) 的额外空间复杂度内完成这个题目吗?(出于对空间复杂度分析的目的,输出数组不被视为额外空间。)

示例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

瓶颈分析:时间复杂度 O(n2)O(n^2),当 n105n \le 10^5 时,101010^{10} 次操作远超时间限制。而且内部循环做了大量重复计算——对于相邻的位置 ii+1,它们共享了大量乘数,但暴力法每次都从头乘起。

优化方向:能否复用已经算过的乘积?如果我们提前算好每个位置的「左边所有数的乘积」和「右边所有数的乘积」,那么每个位置的结果就是两者相乘,只需 O(1)O(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
  • 时间复杂度:O(n)O(n),三次遍历
  • 空间复杂度:O(n)O(n),需要 leftright 两个额外数组

虽然时间已经最优,但题目进阶要求提出:能否只用 O(1)O(1) 额外空间?

空间优化:用输出数组替代前缀数组#

观察上述代码,leftans 计算时机可以合并——我们先用 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 数组一开始存放前缀积,然后从右向左遍历时「就地」乘上后缀积,一举两得。由于输出数组不计入额外空间,我们实现了 O(1)O(1) 额外空间。

代码优化:双指针一次遍历#

上面的解法需要两次独立的遍历。有没有办法在一次遍历中同时完成前后缀积的累积?

答案是可以的。我们维护两个变量 lr,分别记录从左到右和从右到左的累积乘积。在每次迭代中,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

逐行解读这段代码的执行逻辑:

  1. 初始化l = r = 1,分别代表「从左到右」和「从右到左」的累积乘积。res 全为 1 作为初始值。
  2. 循环体内
    • res[i] *= l:将 i 位置乘上它左边所有元素的累积积
    • res[n - i - 1] *= r:将对称位置 n-i-1 乘上它右边所有元素的累积积
    • l *= x:把当前元素 x 纳入左边的累积
    • r *= nums[n - i - 1]:把对称位置的元素纳入右边的累积
  3. 循环结束时,res[i] 恰好被乘上了「左边累积 × 右边累积」,即除自身之外的乘积。

🧠 直觉理解:想象你从数组两端同时出发。l 是你从左边一路捡起的元素的乘积,每到一个新位置,先把这个位置的结果乘上 l(这是「我左边的东西」),然后再把当前元素「装进」l 给后面的位置用。r 从右边做同样的事。两个方向在一次循环中交错推进,互不干扰,因为它们操作的是数组的两个不同位置。

执行过程可视化#

nums = [1, 2, 3, 4] 为例,追踪 lrres 的变化:

ixn-i-1操作前 resl (操作前)r (操作前)操作后 resl (操作后)r (操作后)
[1, 1, 1, 1]11
013[1, 1, 1, 1]11[1, 1, 1, 1]14
122[1, 1, 1, 1]14[1, 1, 4, 1]212
231[1, 1, 4, 1]212[1, 12, 8, 1]624
340[1, 12, 8, 1]624[24, 12, 8, 6]2424

最终 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] 都恰好被乘上了它左边和右边的所有元素——不多不少。

复杂度#

  • 时间复杂度O(n)O(n),只需一次遍历。
  • 空间复杂度O(1)O(1)(输出数组不计入额外空间),仅使用了 lr 两个变量。

O(n)O(n) 时间已是最优——因为输出结果需要为 nn 个位置分别计算,至少需要遍历一次数组。

代码优化#

三种写法的对比#

对比项左右数组法两次遍历法单次双指针法
遍历次数3 次2 次1 次
额外空间O(n)O(n)O(1)O(1)O(1)O(1)
代码行数~12 行~11 行~9 行
可读性⭐⭐⭐⭐⭐(最直观)⭐⭐⭐⭐⭐⭐⭐(需理解双指针交错逻辑)
推荐场景面试中先展示思路面试中优化后的标准解追求极简和 Pythonic

三种写法的时间复杂度均为 O(n)O(n),差异在于常数因子和代码风格。面试中建议从「左右数组法」讲起,展示清晰的思维递进过程,最后落地到「两次遍历法」或「单次双指针法」。

💡 面试建议:面试官更看重你的思维过程而非最终代码。先说「我们需要前缀积和后缀积」,用左右数组法写出第一版,再逐步优化——这个过程本身就能展现你的问题分解和优化能力。

处理零值的情况#

nums 中可能包含 0(如示例2 [-1, 1, 0, -3, 3]),但上述算法天然能正确处理——因为前缀积会正常累积 0,后缀积同理。唯一需要注意的是:如果题目不保证乘积在 32 位范围内(本题保证了),可能需要考虑溢出问题。

总结#

「除自身以外数组的乘积」的核心在于将问题拆解为两个独立维度——前缀积后缀积——然后合并。这个思维模式在数组问题中非常经典:

  • 前缀和 / 前缀积:预处理每个位置「从开头到这里的累积信息」
  • 后缀和 / 后缀积:预处理每个位置「从这里到末尾的累积信息」
  • 合并:在每个位置组合前后信息得到答案

🧠 核心收获:当你遇到「除了某个元素之外的所有元素」这种问题时,第一反应就应该是——把这个元素从中间”挖掉”,然后分别看它的左边和右边。前缀 + 后缀的组合模式能帮你把看似需要 O(n2)O(n^2) 的问题降到 O(n)O(n)

解决本题后,推荐挑战以下延伸题目——它们都涉及「前缀 + 后缀」或「预处理 + 合并」的思想:

  • 42. 接雨水:每个位置能接多少水取决于它左边的最大高度和右边的最大高度——正是前缀最大值 + 后缀最大值的组合
  • 135. 分发糖果:每个孩子分多少糖取决于他左边和右边的相对排名——两次遍历,分别满足左右约束
  • 152. 乘积最大子数组:将前缀积的思想延伸到子数组问题,乘积的符号翻转带来了新的挑战
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

除自身以外数组的乘积
https://www.hygen.red/posts/hot100/05-普通数组/016除自身以外数组的乘积/
作者
Hygen
发布于
2026-07-03
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录