LeetCode数组乘积问题:O(1)空间解法与工程优化

📅 发布时间:2026/8/9 11:21:25
LeetCode数组乘积问题:O(1)空间解法与工程优化
1. 问题背景与核心挑战这道来自LeetCode Hot100的题目除自身以外数组的乘积Product of Array Except Self看似简单实则暗藏玄机。题目要求我们为给定数组nums的每个元素nums[i]计算数组中除nums[i]之外所有元素的乘积并将结果存入新数组返回。关键在于必须在不使用除法运算的情况下实现O(n)时间复杂度且只能使用常数空间输出数组不算额外空间。我第一次遇到这个问题时直觉反应是用除法先计算整个数组的乘积然后对每个元素做除法。但题目明确禁止除法操作这就像让你做菜但不准用刀——必须寻找替代方案。更棘手的是当数组包含0时除法方案会直接崩溃。例如数组[1,0,3,4]总乘积为0对0做除法会导致未定义行为。2. 暴力解法与空间换时间方案2.1 最直观的暴力解法最直接的思路是对每个元素nums[i]遍历数组计算其他所有元素的乘积def productExceptSelf(nums): n len(nums) output [1] * n for i in range(n): for j in range(n): if j ! i: output[i] * nums[j] return output这种方法时间复杂度高达O(n²)在LeetCode上会超时。但它的价值在于帮我们理清问题本质——每个输出元素都是输入数组在某个缺口下的乘积。2.2 前缀与后缀乘积的优化更聪明的做法是预先计算每个元素左侧所有数的乘积前缀积和右侧所有数的乘积后缀积然后将两者相乘def productExceptSelf(nums): n len(nums) left, right [1]*n, [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[i1] * nums[i1] # 合并结果 return [left[i]*right[i] for i in range(n)]这个方案时间复杂度降到了O(n)但需要O(n)额外空间存储前缀和后缀数组。虽然能通过LeetCode测试但距离最优解还有一步之遥。3. 空间复杂度优化到O(1)的终极方案3.1 利用输出数组作为临时存储真正的突破在于意识到我们可以复用输出数组。具体分两步第一次遍历计算每个元素的前缀积直接存入输出数组第二次反向遍历时动态计算后缀积并与已存储的前缀积相乘def productExceptSelf(nums): n len(nums) output [1] * n # 计算前缀积并存入output for i in range(1, n): output[i] output[i-1] * nums[i-1] # 动态计算后缀积并与前缀积相乘 R 1 # 初始后缀积 for i in range(n-1, -1, -1): output[i] output[i] * R R * nums[i] # 更新后缀积 return output这个方案的精妙之处在于第一次正向遍历时output[i]已经包含了nums[0..i-1]的乘积反向遍历时R变量动态维护nums[i1..n-1]的乘积两者相乘正好得到除nums[i]外所有元素的乘积3.2 边界条件处理在实际编码中有几个关键点需要注意初始化output[0] 1因为第一个元素没有前缀反向遍历时R初始化为1因为最后一个元素没有后缀更新R要在计算当前output[i]之后否则会包含nums[i]自身4. 算法复杂度分析与变种问题4.1 时间复杂度与空间复杂度时间复杂度两次遍历O(2n) O(n)空间复杂度除输出数组外只用了常数空间O(1)4.2 相关变种问题包含零的情况处理当数组中有1个零时除零位置外的结果都应为零有多个零时所有结果都为零。我们的方案天然支持这种情况。乘积可能溢出如果题目说明乘积可能很大可以考虑取模运算或使用对数转换但会引入浮点精度问题。多维扩展类似思想可以扩展到二维矩阵计算除当前行列外所有元素的乘积。5. 实际应用场景与工程实践5.1 真实世界中的应用这种乘积计算模式在以下场景很常见图像处理中的局部滤波器计算金融分析中的滚动收益率计算推荐系统中的协同过滤排除自身影响5.2 工程实现中的优化技巧循环展开对于固定大小的数组可以手动展开循环减少分支预测错误并行计算前缀积和后缀积的计算可以并行化SIMD指令现代CPU支持单指令多数据操作可以加速乘积计算# 使用numpy的向量化操作虽然不符合题目限制但实际工程中很有用 import numpy as np def productExceptSelf(nums): nums np.array(nums) return (np.prod(nums) / nums).astype(int)6. 不同语言实现对比6.1 C实现vectorint productExceptSelf(vectorint nums) { int n nums.size(); vectorint output(n, 1); // 前缀积 for(int i 1; i n; i) output[i] output[i-1] * nums[i-1]; // 后缀积 int R 1; for(int i n-1; i 0; --i) { output[i] * R; R * nums[i]; } return output; }6.2 Java实现public int[] productExceptSelf(int[] nums) { int n nums.length; int[] output new int[n]; Arrays.fill(output, 1); // 前缀积 for(int i 1; i n; i) output[i] output[i-1] * nums[i-1]; // 后缀积 int R 1; for(int i n-1; i 0; i--) { output[i] * R; R * nums[i]; } return output; }6.3 JavaScript实现function productExceptSelf(nums) { const n nums.length; const output new Array(n).fill(1); // 前缀积 for(let i 1; i n; i) output[i] output[i-1] * nums[i-1]; // 后缀积 let R 1; for(let i n-1; i 0; i--) { output[i] * R; R * nums[i]; } return output; }7. 常见错误与调试技巧7.1 典型错误模式初始化错误忘记将output数组初始化为1导致乘积错误边界处理不当没有正确处理第一个和最后一个元素的特殊情况更新顺序错误先更新R再计算output[i]导致包含当前元素7.2 调试方法小规模测试先用长度为2或3的数组测试如[1,2]或[1,2,3]打印中间结果在两次遍历之间打印output数组检查前缀积极端情况测试包含0的数组、全1数组、负数和正数混合数组8. 算法思维拓展这个问题展示了计算机科学中常见的空间换时间和时间换空间的权衡。更高级的变种包括多趟扫描算法类似思想可用于解决除自身以外的最大/最小值等问题流式处理版本如果数组是数据流如何实时维护乘积分布式版本如何在MapReduce框架下实现这种计算我在实际工程中曾用类似思路优化过一个实时推荐系统的特征计算模块将计算时间从O(n²)降到O(n)使系统能够处理十倍以上的数据量。这种优化在数据量大时效果尤为明显。