☰
LeetCode 238题解:不用除法实现数组元素乘积
2026/10/26 13:02:24 网站建设 项目流程

1. 问题背景与核心挑战

这道来自LeetCode Hot 100的题目(编号238)要求我们实现一个算法:给定一个整数数组nums,返回一个数组answer,其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。题目要求不能使用除法,并且在O(n)时间复杂度内完成。

这个问题的难点在于:

  • 禁止使用除法运算(否则直接计算总乘积后除以当前元素即可)
  • 需要处理数组中可能存在的0元素
  • 要求时间复杂度严格控制在O(n)
  • 空间复杂度除输出数组外只能使用常数空间

2. 解决方案设计思路

2.1 前后缀乘积分解法

最优雅的解决方案是将问题分解为前缀乘积和后缀乘积两部分:

  1. 前缀乘积:从左到右遍历,计算每个位置左侧所有元素的乘积
  2. 后缀乘积:从右到左遍历,计算每个位置右侧所有元素的乘积
  3. 最终结果:将每个位置的前缀乘积和后缀乘积相乘

这种方法完美满足题目所有约束条件:

  • 时间复杂度:两次遍历O(n)
  • 空间复杂度:除输出数组外只用了两个临时变量
  • 无需除法运算
  • 能正确处理0元素

2.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

3. 关键实现细节解析

3.1 边界条件处理

  • 空数组:直接返回空数组
  • 单元素数组:返回[1](因为没有其他元素)
  • 包含0的数组:算法能自然处理,因为乘积会通过前后缀传播

3.2 空间优化技巧

虽然题目允许使用输出数组不算额外空间,但我们还可以进一步优化:

  • 第一个循环直接用answer数组存储前缀乘积
  • 第二个循环使用一个变量动态计算后缀乘积
  • 这样连临时数组都不需要

3.3 复杂度分析

  • 时间复杂度:O(n),两次线性遍历
  • 空间复杂度:O(1)(不考虑输出数组)

4. 常见问题与解决方案

4.1 为什么不能用除法?

题目明确禁止使用除法,主要考察的是对数组操作的技巧。此外,当数组中有0元素时,除法会导致除零错误。

4.2 如何处理多个0的情况?

  • 如果有两个及以上0,所有结果都将是0
  • 如果只有一个0,只有该位置的乘积不为0
  • 算法能自动处理这些情况

4.3 为什么选择前后缀分解?

这是典型的空间换时间策略:

  • 预计算前后缀乘积避免重复计算
  • 将O(n^2)的暴力解法优化为O(n)
  • 符合题目对时间复杂度的严格要求

5. 算法扩展与变种

5.1 允许使用除法的情况

如果允许使用除法,可以:

  1. 计算总乘积
  2. 对于每个元素,用总乘积除以当前元素 但需要注意处理0的情况

5.2 三维数组乘积问题

类似的思路可以扩展到多维数组,计算每个元素"周围"的乘积,只需要增加维度的遍历方向。

5.3 乘积最大子数组问题

这是LeetCode另一道经典题目(152),可以使用类似的分解思想,但要额外考虑负负得正的情况。

6. 实际应用场景

这种前后缀分解的技巧在以下场景很有用:

  • 图像处理中的邻域计算
  • 信号处理中的滑动窗口统计
  • 数据库查询优化中的预计算
  • 金融分析中的移动平均计算

7. 代码优化与测试用例

7.1 优化后的Python实现

def productExceptSelf(nums): res = [1] * len(nums) prefix = 1 for i in range(len(nums)): res[i] = prefix prefix *= nums[i] postfix = 1 for i in range(len(nums)-1, -1, -1): res[i] *= postfix postfix *= nums[i] return res

7.2 关键测试用例

test_cases = [ ([1,2,3,4], [24,12,8,6]), ([-1,1,0,-3,3], [0,0,9,0,0]), ([0,0], [0,0]), ([1], [1]), ([2,3], [3,2]) ]

8. 不同语言实现要点

8.1 C++实现注意事项

vector<int> productExceptSelf(vector<int>& nums) { vector<int> res(nums.size(), 1); int prefix = 1; for(int i = 0; i < nums.size(); i++) { res[i] = prefix; prefix *= nums[i]; } int postfix = 1; for(int i = nums.size()-1; i >= 0; i--) { res[i] *= postfix; postfix *= nums[i]; } return res; }

注意:

  • 使用vector容器更安全
  • 注意整数溢出问题(题目没说数字范围)
  • 可以使用size_t代替int作为索引

8.2 Java实现特点

public int[] productExceptSelf(int[] nums) { int[] res = new int[nums.length]; Arrays.fill(res, 1); int prefix = 1; for(int i = 0; i < nums.length; i++) { res[i] = prefix; prefix *= nums[i]; } int postfix = 1; for(int i = nums.length-1; i >= 0; i--) { res[i] *= postfix; postfix *= nums[i]; } return res; }

注意:

  • 数组初始化要用Arrays.fill
  • 没有元组解包,代码稍显冗长
  • 同样要注意整数溢出问题

9. 算法可视化理解

想象数组元素排成一排:

  1. 第一趟从左到右:每个位置记录左边所有元素的乘积
  2. 第二趟从右到左:每个位置乘以右边所有元素的乘积
  3. 最终结果就是左右乘积的合并

例如数组[1,2,3,4]:

  • 前缀乘积:[1,1,2,6]
  • 后缀乘积:[24,12,4,1]
  • 最终结果:[24,12,8,6]

10. 性能优化进阶

10.1 并行计算优化

对于超大数组,可以考虑:

  • 将数组分段
  • 并行计算各段的前后缀乘积
  • 合并结果

10.2 内存访问优化

  • 尽量保证内存访问的连续性
  • 对小数组使用寄存器变量
  • 避免不必要的内存分配

10.3 SIMD指令利用

现代CPU支持单指令多数据操作,可以:

  • 使用SIMD指令并行计算乘积
  • 特别适合处理大型数组

11. 面试常见问题

11.1 如何解释算法思路?

建议回答结构:

  1. 明确问题约束条件
  2. 提出暴力解法及其缺点
  3. 介绍前后缀分解的优化思路
  4. 分析时间/空间复杂度
  5. 讨论边界条件和特殊情况

11.2 可能的follow-up问题

  • 如果允许使用除法会怎样?
  • 如何处理浮点数数组?
  • 如何扩展到多维数组?
  • 如果内存非常有限怎么办?

12. 类似题目推荐

  1. LeetCode 42:接雨水(同样使用前后缀分解)
  2. LeetCode 152:乘积最大子数组
  3. LeetCode 135:分发糖果
  4. LeetCode 53:最大子数组和
  5. LeetCode 121:买卖股票的最佳时机

13. 个人实现心得

在实际编码中发现几个关键点:

  1. 初始时answer数组要初始化为1而不是0
  2. 第二个循环要从后往前遍历
  3. 后缀乘积的计算要与前缀乘积相乘,不是赋值
  4. 注意乘积可能溢出(虽然题目没说)

一个容易犯的错误是在第二个循环中错误地使用:

answer[i] = right_product # 错误!应该是 *=

这会覆盖之前计算的前缀乘积。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询