1. 问题背景与核心挑战
LeetCode 238题"除自身以外数组的乘积"是算法面试中的经典题目,要求在不使用除法运算的情况下,计算数组中每个元素除自身外所有其他元素的乘积。这道题出现在热门100题(Hot100)列表中,考察的是对数组操作和空间复杂度的掌控能力。
给定一个整数数组nums,返回一个数组answer,使得answer[i]等于nums中除nums[i]之外所有元素的乘积。题目明确要求:
- 时间复杂度必须为O(n)
- 不能使用除法运算
- 空间复杂度除输出数组外需为O(1)
注意:这是少数明确限制空间复杂度的题目之一,常规的左右乘积数组方法需要O(n)额外空间,不符合进阶要求。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础解法:左右乘积数组
2.1 算法思路解析
最直观的解法是构建两个辅助数组:
- left[i]表示nums[i]左侧所有元素的乘积
- right[i]表示nums[i]右侧所有元素的乘积
最终answer[i] = left[i] * right[i]。这种方法时间复杂度O(n),空间复杂度O(n)。
python复制def productExceptSelf(nums):
n = len(nums)
left, right, answer = [1]*n, [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[i+1] * nums[i+1]
for i in range(n):
answer[i] = left[i] * right[i]
return answer
2.2 复杂度分析
- 时间复杂度:三次单循环,O(3n) → O(n)
- 空间复杂度:两个辅助数组,O(2n) → O(n)
虽然满足基本要求,但空间复杂度不符合进阶标准。面试中可能被要求优化。
3. 进阶解法:空间复杂度O(1)
3.1 优化思路突破
关键观察点:输出数组answer不算额外空间。我们可以:
- 先用answer存储左侧乘积
- 再用一个变量动态计算右侧乘积并更新answer
python复制def productExceptSelf(nums):
n = len(nums)
answer = [1] * n
# 计算左侧乘积
for i in range(1, n):
answer[i] = answer[i-1] * nums[i-1]
# 动态计算右侧乘积
R = 1
for i in range(n-1, -1, -1):
answer[i] = answer[i] * R
R *= nums[i]
return answer
3.2 关键步骤详解
-
第一次遍历:从左到右填充左侧乘积
- answer[0] = 1(第一个元素左侧无元素)
- answer[i] = answer[i-1] * nums[i-1]
-
第二次遍历:从右到左计算右侧乘积
- 使用变量R累积右侧乘积
- 初始R=1(最后一个元素右侧无元素)
- 逆向遍历时,answer[i] *= R,然后R *= nums[i]
实操技巧:第二次遍历时,先更新answer再更新R,顺序不能颠倒。
4. 边界条件与异常处理
4.1 特殊输入情况
- 空数组:应返回空数组
- 单元素数组:根据题意应返回[1]
- 含零数组:
- 单个零:[0,1,2] → [2,0,0]
- 多个零:[0,0,3] → [0,0,0]
4.2 代码健壮性优化
python复制def productExceptSelf(nums):
if not nums: return []
if len(nums) == 1: return [1]
n = len(nums)
answer = [1] * n
# 左侧乘积
for i in range(1, n):
answer[i] = answer[i-1] * nums[i-1]
# 右侧乘积
R = 1
for i in range(n-1, -1, -1):
answer[i] *= R
R *= nums[i]
return answer
5. 同类问题扩展
5.1 变种问题示例
-
允许使用除法时:
- 计算总乘积,然后answer[i] = total / nums[i]
- 需单独处理nums[i]=0的情况
-
多维数组乘积:
- 扩展到矩阵每行/每列乘积
- 需要更高维度的前缀积思想
5.2 实际应用场景
- 图像处理中的滤波器计算
- 金融分析中的滚动乘积计算
- 概率统计中的条件概率计算
6. 算法复杂度对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力法 | O(n²) | O(1) | 不推荐 |
| 左右乘积数组 | O(n) | O(n) | 基础解法 |
| 优化空间版本 | O(n) | O(1) | 面试最佳答案 |
| 使用除法(如果允许) | O(n) | O(1) | 特殊情况 |
7. 常见错误与调试技巧
7.1 典型错误案例
-
初始化错误:
- answer数组未初始化为1
- R变量初始值不为1
-
遍历顺序错误:
- 第二次遍历方向应为从右到左
- 更新R和answer的顺序颠倒
-
边界处理缺失:
- 未处理nums长度为1的情况
- 多个零的情况处理不当
7.2 调试建议
-
打印中间变量:
python复制print(f"i={i}, answer={answer}, R={R}") -
小规模测试用例:
- [1,2,3,4]
- [0,1,2]
- [1,0,3,0]
-
可视化辅助:
- 绘制乘积计算流程图
- 标记每个步骤的变量状态
8. 不同语言实现要点
8.1 Java实现
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 R = 1;
for (int i = n-1; i >= 0; i--) {
answer[i] *= R;
R *= nums[i];
}
return answer;
}
8.2 C++实现
cpp复制vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> answer(n, 1);
// 左侧乘积
for (int i = 1; i < n; ++i) {
answer[i] = answer[i-1] * nums[i-1];
}
// 右侧乘积
int R = 1;
for (int i = n-1; i >= 0; --i) {
answer[i] *= R;
R *= nums[i];
}
return answer;
}
9. 算法思维训练建议
-
前缀思想延伸:
- 前缀和数组
- 差分数组
- 二维前缀和
-
空间优化套路:
- 输出数组复用
- 双指针压缩空间
- 位运算优化
-
同类题目练习:
- LeetCode 42. 接雨水
- LeetCode 152. 乘积最大子数组
- LeetCode 135. 分发糖果
10. 面试实战技巧
-
解题步骤建议:
- 先描述暴力解法
- 提出左右乘积数组优化
- 最后优化空间复杂度
-
沟通要点:
- 明确问题约束条件
- 解释每个优化步骤的思路
- 讨论时间/空间复杂度
-
常见follow-up:
- 如果允许使用除法怎么做?
- 如何处理大数溢出?
- 扩展到二维数组怎么做?
在实际面试中,我通常会先写出基础解法,然后分析空间使用情况,自然地引出优化方案。记住要向面试官展示你的思考过程,这比直接给出最优解更重要。
