1. 题目解析与核心思路
这道LeetCode 238题要求我们计算数组中每个元素除自身外其他元素的乘积。乍看简单,但题目明确要求不能使用除法运算,且时间复杂度需控制在O(n),这就排除了暴力解法和先求总积再除的取巧方法。
关键限制条件:时间复杂度O(n),空间复杂度O(n),进阶要求空间复杂度O(1)(输出数组不计入空间复杂度)
1.1 暴力解法为什么不行
最直观的解法是对于每个元素nums[i],遍历数组计算其他所有元素的乘积:
python复制def productExceptSelf(nums):
n = len(nums)
res = []
for i in range(n):
product = 1
for j in range(n):
if j != i:
product *= nums[j]
res.append(product)
return res
这种方法时间复杂度O(n²),在n=1e5时会超时,完全无法通过测试用例。
1.2 除法为什么被禁止
如果允许使用除法,我们可以先计算所有元素的乘积total,然后对于每个nums[i],结果就是total/nums[i]。但题目明确禁止使用除法,主要是考察真正的算法能力而非取巧。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 双指针法核心思路
2.1 前后缀乘积分解
关键突破点在于发现:对于nums[i],其结果为"左边所有元素的乘积" × "右边所有元素的乘积"。这让我们可以将问题分解为:
- 计算每个元素左侧所有元素的乘积(前缀积)
- 计算每个元素右侧所有元素的乘积(后缀积)
- 将前缀积和后缀积相乘得到最终结果
2.2 空间优化技巧
常规实现需要两个数组存储前缀积和后缀积,但我们可以优化:
- 先用输出数组存储前缀积
- 再用一个变量动态计算后缀积,同时更新输出数组
这样空间复杂度就从O(n)降到了O(1)(输出数组不计入空间复杂度)
3. 完整代码实现与逐行解析
3.1 Python实现
python复制def productExceptSelf(nums):
n = len(nums)
res = [1] * n
# 计算前缀积
prefix = 1
for i in range(n):
res[i] = prefix
prefix *= nums[i]
# 计算后缀积并合并结果
suffix = 1
for i in range(n-1, -1, -1):
res[i] *= suffix
suffix *= nums[i]
return res
3.2 关键代码解析
- 初始化结果数组res为全1,长度与nums相同
- 第一轮正向遍历:
- res[i]存储nums[0..i-1]的乘积(即i左侧所有元素的积)
- prefix变量累积左侧乘积
- 第二轮反向遍历:
- suffix变量累积右侧乘积
- res[i]乘以suffix得到最终结果
- 时间复杂度O(n)(两轮遍历),空间复杂度O(1)(除输出数组外只用了两个变量)
4. 边界条件与特殊案例
4.1 必须考虑的边界情况
- 数组长度为1:应返回[1]
- 数组包含0的情况:
- 单个0:[0,1,2] → [2,0,0]
- 多个0:[0,0,3] → [0,0,0]
- 大数情况:题目保证乘积在32位整数范围内
4.2 测试用例验证
python复制print(productExceptSelf([1,2,3,4])) # [24,12,8,6]
print(productExceptSelf([-1,1,0,-3,3])) # [0,0,9,0,0]
print(productExceptSelf([0,0])) # [0,0]
print(productExceptSelf([5])) # [1]
5. 常见错误与调试技巧
5.1 新手常犯错误
- 初始化res为全0:会导致所有乘积为0
- 忘记处理边界元素:第一个元素没有前缀,最后一个元素没有后缀
- 后缀积计算方向错误:必须从右向左遍历
- 空间优化时覆盖问题:两轮遍历必须分开进行
5.2 调试建议
- 打印中间变量:
python复制print(f"i={i}, prefix={prefix}, res={res}") - 用小数组手动演算:
- 例如[1,2,3]:
- 前缀积:[1, 1, 2]
- 后缀积:[6, 3, 1]
- 结果:[6, 3, 2]
- 例如[1,2,3]:
6. 算法复杂度分析
6.1 时间复杂度
- 两轮独立的遍历:O(n) + O(n) = O(n)
- 每个元素被访问两次
6.2 空间复杂度
- 输出数组不计入(题目要求)
- 只使用了prefix和suffix两个变量:O(1)
7. 同类问题拓展
7.1 相似题目推荐
- LeetCode 42. 接雨水(同样使用前后缀分解)
- LeetCode 152. 乘积最大子数组
- LeetCode 135. 分发糖果(双向遍历)
7.2 变种问题思考
- 如果允许使用除法,如何修改解法?
- 如果要求结果数组也计入空间复杂度,如何实现?
- 如果数组非常大无法全部装入内存,如何处理?
8. 面试实战技巧
8.1 面试官可能追问的问题
- 为什么这个方法能保证O(n)时间复杂度?
- 如何处理数组中有0的情况?
- 能否进一步优化空间复杂度?
- 如果数组元素是浮点数,算法需要调整吗?
8.2 回答策略
- 先明确问题要求和限制条件
- 从暴力解法出发,分析其缺陷
- 逐步优化,解释每个优化步骤的考虑
- 最后讨论边界情况和可能的变种
9. 个人实现心得
在实际编码时,我发现最容易出错的是后缀积计算的方向。有次面试中我因为紧张写成了从左往右计算,导致结果完全错误。后来养成了习惯:对于双向遍历的问题,一定先用小数组手动模拟过程。
另一个实用技巧是:当不确定循环边界时,先写出数学表达式。比如对于nums[i],结果是:
product(nums[0..i-1]) * product(nums[i+1..n-1])
这样就能明确知道:
- 前缀积需要计算到i-1
- 后缀积需要从i+1开始
- i=0和i=n-1时的边界情况
最后分享一个调试技巧:当算法出现问题时,不要急着改代码,先用纸笔模拟一个小案例(比如长度为3的数组)的完整执行过程,往往能快速定位问题。
