今天我们来探讨一个经典的数组操作问题:如何计算数组中每个元素除自身外所有其他元素的乘积。这个问题看似简单,但要想出最优解却需要一些巧妙的思路。
问题的正式描述是:给定一个整数数组nums,返回一个数组ans,其中ans[i]等于nums中除nums[i]之外所有元素的乘积。题目要求不能使用除法运算,并且在O(n)时间复杂度内完成。
举个例子,如果输入数组是[1,2,3,4],那么输出应该是[24,12,8,6]。因为:
最直观的解法是对每个元素,遍历整个数组计算其他所有元素的乘积:
python复制def productExceptSelf(nums):
n = len(nums)
ans = [1] * n
for i in range(n):
for j in range(n):
if j != i:
ans[i] *= nums[j]
return ans
这种方法的时间复杂度是O(n²),对于大数组来说效率太低。我们需要寻找更优的解法。
有人可能会想到先计算所有元素的乘积,然后对每个元素用总积除以它本身:
python复制def productExceptSelf(nums):
total = 1
for num in nums:
total *= num
return [total // num for num in nums]
但这种方法有两个问题:
更聪明的方法是使用前缀积和后缀积的概念:
对于数组中的每个元素,它的结果就是它的前缀积乘以后缀积。
python复制def productExceptSelf(nums):
n = len(nums)
pre = [1] * n # 前缀积数组
back = [1] * n # 后缀积数组
ans = []
# 计算前缀积
for i in range(1, n):
pre[i] = pre[i-1] * nums[i-1]
# 计算后缀积
for i in range(n-2, -1, -1):
back[i] = back[i+1] * nums[i+1]
# 计算结果
for i in range(n):
ans.append(pre[i] * back[i])
return ans
上面的解法使用了两个额外的数组(pre和back),空间复杂度是O(n)。我们可以进一步优化,只使用一个输出数组:
python复制def productExceptSelf(nums):
n = len(nums)
ans = [1] * n
# 先计算前缀积并直接存入ans
for i in range(1, n):
ans[i] = ans[i-1] * nums[i-1]
# 使用一个变量来跟踪后缀积
suffix = 1
for i in range(n-1, -1, -1):
ans[i] *= suffix
suffix *= nums[i]
return ans
这种优化后的方法空间复杂度降为O(1)(不考虑输出数组的空间)。
在实际编码中,我们需要特别注意边界条件:
我们的解法已经天然处理了这些边界情况,因为:
无论是否优化空间,算法都只需要:
因此时间复杂度是O(n),非常高效。
这种算法在实际中有多种应用:
常见错误是忘记将pre和back数组初始化为1。如果初始化为0,所有乘积都会变成0。
前缀积必须从左到右计算,后缀积必须从右到左计算。如果顺序弄反,结果会不正确。
特别注意循环的起始和结束索引:
如果题目允许使用除法,我们可以:
对于二维数组,如何计算每个元素除自身外所有元素的乘积?这需要更复杂的前缀和后缀计算方法。
对于非常大的数组,可以考虑将前缀积和后缀积的计算并行化,进一步提升性能。
好的算法实现需要全面的测试案例:
python复制# 正常情况
assert productExceptSelf([1,2,3,4]) == [24,12,8,6]
# 包含0的情况
assert productExceptSelf([1,0,3,4]) == [0,12,0,0]
# 单元素数组
assert productExceptSelf([5]) == [1]
# 空数组
assert productExceptSelf([]) == []
# 负数情况
assert productExceptSelf([-1,2,-3,4]) == [-24,12,-8,6]
在实际编码中,我发现这个算法最巧妙的部分在于将问题分解为前缀和后缀两部分。这种"分而治之"的思想在很多算法问题中都很有效。
有几个值得注意的细节:
这个算法也展示了预处理的重要性——通过预先计算并存储中间结果,我们可以避免大量的重复计算,这是优化算法性能的常用技巧。