1. 问题解析与解法概述
今天我们来探讨LeetCode热题第238题——"除自身以外数组的乘积"。这道题看似简单,但其中蕴含的算法优化思路非常经典,也是面试中的高频考点。题目要求我们计算一个数组中每个元素除自身外所有其他元素的乘积,并且有严格的时间和空间复杂度限制。
1.1 题目核心约束
首先明确题目给出的三个关键限制条件:
- 禁止使用除法运算:这意味着我们不能简单地先计算整个数组的乘积,然后对每个元素进行除法操作。这种看似直观的方法被明确禁止。
- 时间复杂度O(n):要求算法必须在单次或有限次遍历中完成计算,不能使用嵌套循环。
- 空间复杂度O(1)(输出数组除外):意味着除了存储结果的数组外,我们几乎不能使用额外的存储空间。
1.2 直观解法及其缺陷
最直观的解法是暴力枚举法:对于数组中的每个元素nums[i],遍历数组计算其他所有元素的乘积。这种方法虽然简单直接,但时间复杂度高达O(n²),完全无法满足题目要求。在实际面试或工程应用中,这种解法通常会被直接否决。
2. 前缀积与后缀积分解法
2.1 核心思路解析
更优的解法是利用前缀积和后缀积的概念。这个思路的关键在于认识到:
- 对于数组中的任意元素nums[i],其对应的answer[i]可以分解为:
- 左边所有元素的乘积(前缀积)
- 右边所有元素的乘积(后缀积)
数学表达式为:answer[i] = prefix[i] × suffix[i]
其中:
- prefix[i] = nums[0] × nums[1] × ... × nums[i-1]
- suffix[i] = nums[i+1] × nums[i+2] × ... × nums[n-1]
2.2 具体实现步骤
-
构建前缀积数组:
- 初始化prefix[0] = 1(因为第一个元素左边没有元素)
- 从左到右遍历数组,计算每个位置的前缀积:
prefix[i] = prefix[i-1] × nums[i-1]
-
构建后缀积数组:
- 初始化suffix[n-1] = 1(因为最后一个元素右边没有元素)
- 从右到左遍历数组,计算每个位置的后缀积:
suffix[i] = suffix[i+1] × nums[i+1]
-
计算结果数组:
- 遍历数组,将对应位置的前缀积和后缀积相乘:
answer[i] = prefix[i] × suffix[i]
2.3 复杂度分析
- 时间复杂度:需要三次独立的线性遍历,总体时间复杂度为O(n)
- 空间复杂度:需要额外的prefix和suffix数组,空间复杂度为O(n)
虽然这种方法满足了时间复杂度的要求,但空间复杂度仍不理想,因为使用了两个额外的数组。
3. 空间优化解法
3.1 优化思路
仔细观察前缀积和后缀积的计算过程,我们可以发现两个关键点:
- 前缀积和后缀积的计算是相互独立的
- 结果数组answer本身可以作为存储空间
基于此,我们可以进行如下优化:
- 第一次遍历:计算前缀积并直接存储在answer数组中
- 第二次遍历:从右向左计算后缀积,并使用一个变量实时维护当前的后缀积
3.2 详细实现步骤
-
初始化answer数组:
- 创建与nums相同长度的answer数组
- answer[0] = 1(第一个元素的前缀积为1)
-
计算前缀积:
- 从左到右遍历数组(从第二个元素开始)
- answer[i] = answer[i-1] × nums[i-1]
-
计算后缀积并完成最终结果:
- 初始化suffix = 1
- 从右到左遍历数组
- answer[i] = answer[i] × suffix
- suffix = suffix × nums[i]
3.3 代码实现(Python版)
python复制def productExceptSelf(nums):
n = len(nums)
answer = [1] * n
for i in range(1, n):
answer[i] = answer[i-1] * nums[i-1]
suffix = 1
for i in range(n-1, -1, -1):
answer[i] *= suffix
suffix *= nums[i]
return answer
3.4 复杂度分析
- 时间复杂度:两次线性遍历,O(n)
- 空间复杂度:除了输出数组外只使用了常数空间,O(1)
这种方法完美满足了题目的所有约束条件,是最优解。
4. 常见问题与调试技巧
4.1 边界条件处理
在实际编码中,有几个边界条件需要特别注意:
- 空数组输入:虽然题目保证数组长度≥2,但实际工程中应该处理
- 数组包含0的情况:虽然不影响算法正确性,但需要理解乘积结果的变化
- 大数溢出:特别是当数组元素较大时,乘积可能超出整数范围
4.2 调试技巧
当实现出现问题时,可以采用以下调试方法:
- 打印中间结果:在两次遍历之间打印answer数组,检查前缀积是否正确
- 小规模测试用例:使用[1,2,3,4]这样的简单数组手动计算预期结果
- 单步调试:跟踪suffix变量的变化,确保反向遍历逻辑正确
4.3 算法变体思考
这个问题还有一些有趣的变体值得思考:
- 如果允许使用除法,如何实现?有什么优缺点?
- 如果数组中有零元素,算法需要如何调整?
- 如何修改算法使其适用于链表的类似操作?
5. 实际应用与扩展
5.1 实际应用场景
这种前缀积/后缀积的算法思想在实际工程中有广泛应用:
- 时间序列分析中的滑动窗口统计
- 图像处理中的邻域操作
- 数据库查询优化中的预计算
5.2 类似题目推荐
掌握这个算法后,可以尝试解决以下类似题目:
- LeetCode 42:接雨水
- LeetCode 152:乘积最大子数组
- LeetCode 135:分发糖果
5.3 性能优化思考
虽然当前算法已经是最优时间复杂度,但在实际工程中还可以考虑:
- 并行计算:前缀积和后缀积计算可以并行进行
- 流式处理:对于无法完全装入内存的大数组,如何分块处理
- 硬件加速:利用GPU或SIMD指令加速乘积计算
6. 个人实现心得
在实际编码实现这个算法时,有几点深刻体会:
- 反向遍历的索引处理要格外小心,特别是Python中的range(n-1, -1, -1)用法
- 初始值的设置很关键,answer[0]=1和suffix=1的初始化保证了边界正确性
- 在面试中,建议先写出前缀/后缀积的O(n)空间解法,再逐步优化到O(1)空间,展示思考过程
这个算法最精妙之处在于复用输出数组来存储中间结果,这种"空间复用"的思想在很多算法问题中都有体现,值得深入理解和掌握。