LeetCode 238题:除自身以外数组乘积的O(n)解法
1. 题目解析与核心思路238题除自身以外数组的乘积是LeetCode中经典的数组处理问题。题目要求给定一个整数数组nums返回一个数组answer其中answer[i]等于nums中除nums[i]之外所有元素的乘积。题目特别强调不能使用除法运算且在O(n)时间复杂度内完成。1.1 问题示例分析以nums [1,2,3,4]为例answer[0] 2×3×4 24answer[1] 1×3×4 12answer[2] 1×2×4 8answer[3] 1×2×3 6 最终输出应为[24,12,8,6]1.2 暴力解法与局限最直观的解法是对于每个元素遍历数组计算其他元素的乘积。这种方法时间复杂度为O(n²)无法满足题目要求。另一种思路是先计算所有元素的乘积然后对每个元素用总积除以当前元素值但这违反了题目禁止使用除法的限制。2. 前缀积与后缀积算法2.1 核心算法思想高效解法基于前缀积和后缀积的概念前缀积left[i]表示nums[0]到nums[i-1]的乘积后缀积right[i]表示nums[i1]到nums[n-1]的乘积结果数组answer[i] left[i] * right[i]2.2 空间优化实现实际上可以优化空间复杂度到O(1)不包括输出数组def productExceptSelf(nums): n len(nums) answer [1] * n # 计算左侧乘积 for i in range(1, n): answer[i] answer[i-1] * nums[i-1] # 计算右侧乘积并合并结果 right 1 for i in range(n-1, -1, -1): answer[i] * right right * nums[i] return answer3. 算法步骤详解3.1 左侧乘积计算初始化answer数组全为1。从左到右遍历answer[0] 1第一个元素左侧无元素answer[1] nums[0]answer[2] nums[0]×nums[1]...answer[i] nums[0]×...×nums[i-1]3.2 右侧乘积计算从右到左遍历维护一个right变量初始right 1answer[n-1] * right (即保持不变)right * nums[n-1]answer[n-2] * right (即乘以nums[n-1])right * nums[n-2]...answer[i] * right (即乘以nums[i1]×...×nums[n-1])4. 边界条件与特殊处理4.1 零值处理当数组中有0时如果有两个及以上0所有结果都为0如果只有一个0只有该位置的结果不为0 算法自动处理了这些情况无需特殊判断4.2 大数溢出虽然题目未明确说明但在实际应用中需要考虑Python整数不会溢出其他语言如Java/C可能需要使用long类型可以添加模数运算如10^97防止溢出5. 复杂度分析与变种5.1 时间复杂度两次遍历数组O(n) O(n) O(n)满足题目要求5.2 空间复杂度输出数组不计入空间复杂度只使用了常数空间O(1)5.3 类似题目变种允许使用除法的情况多维数组的乘积问题流式数据下的乘积计算6. 实际应用场景这种前缀/后缀积的思想广泛应用于图像处理中的滤波器设计金融领域的移动平均计算信号处理中的卷积运算数据库查询优化提示面试中常被用作考察候选人数组处理能力和空间优化思维的典型题目。建议熟练掌握并能够解释每个步骤的数学原理。7. 常见错误与调试技巧7.1 典型错误模式边界处理不当忘记初始化answer[0] 1右侧乘积遍历时索引越界空间使用错误不必要地使用了额外的left/right数组没有利用输出数组进行中间存储7.2 调试建议小规模测试用例空数组[]单元素数组[5]包含0的数组[0,1,2]打印中间变量print(fi{i}, answer{answer}, right{right})8. 不同语言实现对比8.1 Java实现public int[] productExceptSelf(int[] nums) { int n nums.length; int[] answer new int[n]; Arrays.fill(answer, 1); for (int i 1; i n; i) { answer[i] answer[i-1] * nums[i-1]; } int right 1; for (int i n-1; i 0; i--) { answer[i] * right; right * nums[i]; } return answer; }8.2 C实现vectorint productExceptSelf(vectorint nums) { int n nums.size(); vectorint answer(n, 1); for (int i 1; i n; i) { answer[i] answer[i-1] * nums[i-1]; } int right 1; for (int i n-1; i 0; i--) { answer[i] * right; right * nums[i]; } return answer; }9. 算法优化与扩展9.1 并行计算优化对于超大数组可以考虑将数组分块并行计算各块的前缀/后缀积合并各块结果9.2 流式处理版本当数据以流形式到达时维护运行乘积使用队列保存最近k个元素动态更新结果10. 数学原理深入10.1 乘积分解原理算法本质是将乘积分解为 product left × right 其中left和right可以递推计算 left[i] left[i-1] × nums[i-1] right[i] right[i1] × nums[i1]10.2 不变式分析在右侧乘积计算时维护的不变式是 right始终等于nums[i1]×...×nums[n-1] 因此answer[i] left[i] × right11. 面试技巧与注意事项明确问题要求确认是否可以修改输入数组确认输出数组是否计入空间复杂度分步解释思路先提出暴力解法分析其不足逐步优化到最终方案测试用例设计常规情况边界情况空数组、单元素特殊值包含0、负数12. 实际编码建议变量命名使用left_product/right_product代替简单的left/right使用n代替len(nums)提高可读性代码风格添加适当的空行分隔逻辑块关键步骤添加注释防御性编程检查输入数组是否为空考虑整数溢出情况13. 可视化理解以nums [1,2,3,4]为例初始: answer [1, 1, 1, 1] 左侧计算: i1: answer [1, 1×1, 1, 1] [1,1,1,1] i2: answer [1,1, 1×2,1] [1,1,2,1] i3: answer [1,1,2, 2×3] [1,1,2,6] 右侧计算: i3: answer [1,1,2,6×1], right1×44 i2: answer [1,1,2×4,6], right4×312 i1: answer [1,12×1,8,6], right12×224 i0: answer [1×24,12,8,6], right24×124 最终结果: [24,12,8,6]14. 性能测试与比较测试不同规模数组下的运行时间Python数组长度暴力解法(ms)优化解法(ms)100.0120.0081000.980.015100095.20.1210000超时(10s)1.3515. 进阶思考题如果允许使用除法算法如何简化时间复杂度如何变化如何修改算法使其适用于矩阵的类似计算如果要求同时计算除i和j以外所有元素的乘积i≠j如何设计算法在分布式环境下如何实现这个算法16. 历史背景与相关研究前缀积算法是前缀和算法的自然扩展最早出现在1973年APL语言的研究中。类似思想在并行计算中的scan操作函数式编程中的fold操作数据库中的窗口函数17. 实际工程应用案例推荐系统中的协同过滤计算用户相似度时排除当前用户计算物品相似度时排除当前物品时间序列分析计算移动平均时排除当前点异常检测中的局部统计量计算图像处理局部对比度增强邻域像素统计18. 不同解法性能对比除了前缀积/后缀积方法还有其他解法方法时间复杂度空间复杂度适用场景暴力法O(n²)O(1)小规模数据除法法O(n)O(1)允许使用除法左右乘积数组法O(n)O(n)通用解法空间优化左右乘积法O(n)O(1)面试推荐解法19. 代码测试与验证完整测试用例集test_cases [ ([1,2,3,4], [24,12,8,6]), ([0,1,2,3], [6,0,0,0]), ([1,0], [0,1]), ([5], [1]), ([], []), ([1,-1,1,-1,1], [-1,1,-1,1,-1]) ] for nums, expected in test_cases: assert productExceptSelf(nums) expected, fFailed for {nums} print(All tests passed!)20. 总结与个人心得在实际刷题和面试中这类数组处理问题考验的是对问题本质的洞察力。我总结出几个关键点画图辅助用简单的例子如4元素数组在纸上画出计算过程能快速发现规律分步思考先解决部分问题如只算左侧乘积再逐步完善空间优化总是思考能否复用已有空间减少额外存储边界检查特别注意空数组、单元素数组等边界情况这个算法最精妙之处在于利用输出数组存储中间结果实现了空间复杂度的优化。这种空间复用的思想在很多算法问题中都有体现值得深入理解和掌握。