ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数组乘积问题:前缀与后缀乘积的O(n)解法

数组乘积问题:前缀与后缀乘积的O(n)解法

1. 问题背景与需求分析

今天要解决的是一个看似简单但暗藏玄机的数组操作问题:给定一个整数数组nums,返回一个数组answer,其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。题目要求不能使用除法,且在O(n)时间复杂度内完成。

这个问题在实际开发中其实有不少应用场景。比如在数据分析时,我们可能需要计算某个指标相对于其他所有指标的权重乘积;在图像处理中,可能需要计算像素点周围邻域值的某种组合。理解这个问题的解法,对培养数组操作的思维很有帮助。

2. 暴力解法与问题分析

最直观的解法当然是暴力计算:对于每个元素nums[i],我们都遍历整个数组,计算其他所有元素的乘积。这种方法的时间复杂度是O(n²),显然不符合题目要求。

def productExceptSelf(nums): n = len(nums) answer = [1] * n for i in range(n): for j in range(n): if j != i: answer[i] *= nums[j] return answer

这种解法虽然简单直接,但在处理大数组时会非常低效。我们需要寻找更优的解决方案。

3. 优化思路:前缀与后缀乘积

3.1 基本思路

我们可以将问题分解为两部分:对于nums[i],其结果是它左边所有数字的乘积乘以右边所有数字的乘积。这样我们就可以通过两次遍历来完成计算:

  1. 第一次从左到右遍历,计算每个元素左边所有元素的乘积(前缀乘积)
  2. 第二次从右到左遍历,计算每个元素右边所有元素的乘积(后缀乘积),并与前缀乘积相乘得到最终结果

3.2 具体实现

def productExceptSelf(nums): n = len(nums) answer = [1] * n # 计算前缀乘积 left_product = 1 for i in range(n): answer[i] = left_product left_product *= nums[i] # 计算后缀乘积并合并结果 right_product = 1 for i in range(n-1, -1, -1): answer[i] *= right_product right_product *= nums[i] return answer

这个算法的时间复杂度是O(n),因为我们只进行了两次线性遍历;空间复杂度是O(1)(不考虑输出数组),因为我们只使用了常数个额外变量。

4. 边界情况与特殊处理

在实际编码时,我们需要考虑一些边界情况:

  1. 数组长度为0或1的情况
  2. 数组中包含0的情况
  3. 乘积可能溢出的情况(特别是使用其他语言如C++时)

对于包含0的情况,我们的算法仍然适用,因为:

  • 如果只有一个0,那么除了这个0的位置外,其他位置的结果都将是0
  • 如果有多个0,那么所有位置的结果都将是0

5. 算法复杂度分析

让我们更详细地分析这个算法的复杂度:

  • 时间复杂度:我们进行了两次独立的遍历,每次都是O(n),所以总时间复杂度是O(2n) = O(n)
  • 空间复杂度:除了输出数组外,我们只使用了常数个额外变量(left_product和right_product),所以额外空间复杂度是O(1)

6. 实际应用与变种问题

这个算法思想可以应用于多种类似场景:

  1. 计算除当前元素外的其他元素的和、最大值等
  2. 二维数组中的类似问题
  3. 滑动窗口乘积问题

例如,如果我们想计算除当前元素外的其他元素的和,也可以使用类似的思路:

def sumExceptSelf(nums): n = len(nums) answer = [0] * n # 前缀和 left_sum = 0 for i in range(n): answer[i] = left_sum left_sum += nums[i] # 后缀和 right_sum = 0 for i in range(n-1, -1, -1): answer[i] += right_sum right_sum += nums[i] return answer

7. 代码优化与注意事项

在实际编码时,有几点需要注意:

  1. 初始化answer数组时可以使用[1]*n,因为乘积的初始值应该是1
  2. 在第二次遍历时,注意是从后往前遍历
  3. 在更新right_product时,要先计算answer[i]再更新right_product

对于其他编程语言,实现方式可能略有不同。比如在C++中:

vector<int> productExceptSelf(vector<int>& nums) { int n = nums.size(); vector<int> answer(n, 1); int left = 1; for (int i = 0; i < n; ++i) { answer[i] = left; left *= nums[i]; } int right = 1; for (int i = n-1; i >= 0; --i) { answer[i] *= right; right *= nums[i]; } return answer; }

8. 测试用例设计

为了验证我们的算法正确性,应该设计多种测试用例:

  1. 常规测试用例:[1,2,3,4] → [24,12,8,6]
  2. 包含0的测试用例:[1,2,0,4] → [0,0,8,0]
  3. 多个0的测试用例:[1,0,3,0] → [0,0,0,0]
  4. 空数组测试用例:[] → []
  5. 单元素数组测试用例:[5] → [1]
  6. 负数测试用例:[-1,2,-3,4] → [-24,12,-8,6]

9. 常见错误与调试技巧

在实现这个算法时,容易犯的错误包括:

  1. 初始值设置错误(乘积应该初始化为1,而不是0)
  2. 遍历顺序错误(第二次应该从后往前遍历)
  3. 更新顺序错误(应该先使用right_product计算answer[i],再更新right_product)
  4. 边界条件处理不当(空数组或单元素数组)

调试时可以:

  1. 打印中间结果,检查前缀乘积和后缀乘积是否正确
  2. 使用小数组手动计算验证
  3. 特别注意0的情况

10. 扩展思考

这个问题还可以进一步扩展:

  1. 如果允许使用除法,如何解决?(先计算总乘积,然后对每个元素用总乘积除以它)
  2. 如果要求空间复杂度严格O(1)(不包括输出数组),如何修改?
  3. 如果数组非常大,无法一次性装入内存,如何处理?
  4. 如果要求多线程实现,如何设计?

对于不允许使用除法的限制,主要是考察对数组操作的掌握程度。使用除法的方法虽然简单,但在遇到0时会比较麻烦(需要特殊处理),而且题目明确禁止使用除法。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进