Files
interview/16-LeetCode Hot 100/除自身以外数组的乘积.md
yasinshaw 5c1c974e88 docs: 改进LeetCode二叉树题目解题思路
按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

1. 二叉树的中序遍历
   - 增加"思路推导"部分,解释递归到迭代的转换
   - 详细说明迭代法的每个步骤
   - 增加执行过程演示和多种解法

2. 二叉树的最大深度
   - 增加"思路推导",对比DFS和BFS
   - 详细解释递归的基准情况
   - 增加多种解法和变体问题

3. 从前序与中序遍历序列构造二叉树
   - 详细解释前序和中序的特点
   - 增加"思路推导",说明如何分治
   - 详细说明切片边界计算

4. 对称二叉树
   - 解释镜像对称的定义
   - 详细说明递归比较的逻辑
   - 增加迭代解法和变体问题

5. 翻转二叉树
   - 解释翻转的定义和过程
   - 详细说明多值赋值的执行顺序
   - 增加多种解法和有趣的故事

6. 路径总和
   - 详细解释路径和叶子节点的定义
   - 说明为什么使用递减而非累加
   - 增加多种解法和变体问题

每个文件都包含:
- 完整的示例和边界条件分析
- 详细的算法流程和图解
- 关键细节说明
- 常见错误分析
- 复杂度分析(详细版)
- 执行过程演示
- 多种解法
- 变体问题
- 总结

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:33:57 +08:00

690 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 除自身以外数组的乘积 (Product of Array Except Self)
LeetCode 238. Medium
## 题目描述
给你一个整数数组 `nums`,返回数组 `answer`,其中 `answer[i]` 等于 `nums` 中除 `nums[i]` 之外其余各元素的乘积。
**题目要求**:请不要使用除法,且在 O(n) 时间复杂度内完成此题。
**示例 1**
```
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
解释:
- answer[0] = 2 * 3 * 4 = 24
- answer[1] = 1 * 3 * 4 = 12
- answer[2] = 1 * 2 * 4 = 8
- answer[3] = 1 * 2 * 3 = 6
```
**示例 2**
```
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
```
**进阶**:你可以在 O(1) 的额外空间复杂度(输出数组不被视为额外空间)内完成此题目吗?
## 思路推导
### 暴力解法分析
**最直观的思路**:对于每个位置,计算其他所有元素的乘积。
```python
def productExceptSelf(nums):
n = len(nums)
answer = []
for i in range(n):
product = 1
for j in range(n):
if j != i:
product *= nums[j]
answer.append(product)
return answer
```
**时间复杂度**O(n²)
- 外层循环O(n)
- 内层循环O(n)
- 总计O(n) × O(n) = O(n²)
**空间复杂度**O(1),不考虑输出数组
**问题分析**
1. 效率低n=10⁵ 时n² 不可接受
2. 重复计算:很多乘积被多次计算
3. 无法利用已知信息
### 优化思考 - 第一步:使用除法
**观察**:如果可以使用除法
```python
total_product = 1
for num in nums:
total_product *= num
answer[i] = total_product / nums[i]
```
**问题**
1. 题目禁止使用除法
2. 如果 nums[i] = 0除法会出错
3. 如果多个 0需要特殊处理
### 优化思考 - 第二步:分离左右乘积
**关键观察**`answer[i] = 左侧乘积 × 右侧乘积`
```
nums = [1, 2, 3, 4]
answer[0] = (空) × (2 × 3 × 4) = 左侧[0] × 右侧[0]
answer[1] = (1) × (3 × 4) = 左侧[1] × 右侧[1]
answer[2] = (1 × 2) × (4) = 左侧[2] × 右侧[2]
answer[3] = (1 × 2 × 3) × (空) = 左侧[3] × 右侧[3]
```
**为什么这样思考?**
- 每个位置的答案可以分解为两部分
- 左侧部分i 之前所有元素的乘积
- 右侧部分i 之后所有元素的乘积
- 两部分独立计算,然后相乘
**优化后的思路**
```python
# 预计算左侧乘积
left = [1] * n
for i in range(1, n):
left[i] = left[i-1] * nums[i-1]
# 预计算右侧乘积
right = [1] * n
for i in range(n-2, -1, -1):
right[i] = right[i+1] * nums[i+1]
# 合并结果
answer = [left[i] * right[i] for i in range(n)]
```
**时间复杂度**O(n)
- 计算左侧O(n)
- 计算右侧O(n)
- 合并结果O(n)
- 总计O(n)
**空间复杂度**O(n)
- 左侧数组O(n)
- 右侧数组O(n)
- 输出数组O(n)
- 总计O(n)
### 优化思考 - 第三步:空间优化
**问题**:题目要求 O(1) 额外空间(输出数组除外)
**关键优化**:用输出数组存储左侧乘积,用变量累积右侧乘积
```python
# 用 answer 存储左侧乘积
answer = [1] * n
for i in range(1, n):
answer[i] = answer[i-1] * nums[i-1]
# 用变量累积右侧乘积,直接更新 answer
right = 1
for i in range(n-1, -1, -1):
answer[i] *= right
right *= nums[i]
```
**为什么这样思考?**
- 输出数组不被视为额外空间
- 右侧乘积可以用一个变量累积
- 从右向左遍历时,边计算边更新
**空间复杂度**O(1)
- 只用了一个变量 `right`
- 输出数组 `answer` 不计入
## 解题思路
### 核心思想
**分离左右乘积**:将 `answer[i]` 分解为左侧乘积和右侧乘积的乘积。
**为什么这样思考?**
1. **分解问题的思想**
- 复杂问题 → 简单子问题
- 每个位置的答案 = 左边所有数的乘积 × 右边所有数的乘积
2. **独立计算的优势**
- 左侧乘积可以从左到右递推计算
- 右侧乘积可以从右到左递推计算
- 两部分互不干扰
3. **空间优化的技巧**
- 用输出数组存储左侧乘积
- 用变量累积右侧乘积
- 边计算边更新,避免额外数组
### 详细算法流程
**步骤1计算左侧乘积存储在 answer 中)**
```python
answer = [1] * n
# answer[i] = nums[0] × ... × nums[i-1]
for i in range(1, n):
answer[i] = answer[i-1] * nums[i-1]
```
**关键点**
- `answer[0] = 1`0 左侧没有元素)
- `answer[i]` 依赖于 `answer[i-1]`
- 递推关系:`answer[i] = answer[i-1] × nums[i-1]`
**示例**
```
nums = [1, 2, 3, 4]
i=0: answer[0] = 1
i=1: answer[1] = answer[0] × nums[0] = 1 × 1 = 1
i=2: answer[2] = answer[1] × nums[1] = 1 × 2 = 2
i=3: answer[3] = answer[2] × nums[2] = 2 × 3 = 6
answer = [1, 1, 2, 6] (左侧乘积)
```
**步骤2计算右侧乘积并更新答案**
```python
right = 1
for i in range(n-1, -1, -1):
# answer[i] *= 右侧乘积
answer[i] *= right
# 更新右侧乘积
right *= nums[i]
```
**关键点**
- `right` 初始为 1最右侧右侧没有元素
- 从右向左遍历
- 递推关系:`right = right × nums[i]`
**示例(续)**
```
nums = [1, 2, 3, 4]
answer = [1, 1, 2, 6] (左侧乘积)
i=3: answer[3] = 6 × 1 = 6, right = 1 × 4 = 4
i=2: answer[2] = 2 × 4 = 8, right = 4 × 3 = 12
i=1: answer[1] = 1 × 12 = 12, right = 12 × 2 = 24
i=0: answer[0] = 1 × 24 = 24, right = 24 × 1 = 24
answer = [24, 12, 8, 6] (最终结果)
```
### 关键细节说明
**细节1为什么 answer[0] = 1**
```python
answer = [1] * n # 所有位置初始化为 1
```
**原因**
- `answer[0]` 表示 nums[0] 左侧所有元素的乘积
- nums[0] 左侧没有元素
- 空乘积定义为 1乘法的单位元
- 类似:`answer[n-1]` 右侧也没有元素right 初始为 1
**细节2为什么是 `answer[i] = answer[i-1] × nums[i-1]`**
```python
# 错误写法
answer[i] = answer[i-1] * nums[i] # 错误!会包含当前元素
# 正确写法
answer[i] = answer[i-1] * nums[i-1] # 正确
```
**原因**
- `answer[i]` 是 nums[i] 左侧的乘积
- 不应该包含 nums[i] 本身
- 示例:
```
nums = [1, 2, 3, 4]
answer[2] = nums[0] × nums[1] = 1 × 2 = 2
不包含 nums[2] = 3
```
**细节3为什么从右向左遍历**
```python
# 正确:从右向左
for i in range(n-1, -1, -1):
answer[i] *= right
right *= nums[i]
# 错误:从左向右
for i in range(n):
answer[i] *= right # right 值不对
right *= nums[i]
```
**原因**
- `right` 是 nums[i] 右侧的乘积
- 必须先计算右侧的值
- 从右向左遍历才能保证 `right` 是正确的
**细节4为什么顺序是先更新 answer再更新 right**
```python
# 正确顺序
answer[i] *= right # 先使用当前的 right
right *= nums[i] # 再更新 right
# 错误顺序
right *= nums[i] # 错误!先更新了
answer[i] *= right # right 包含了 nums[i] 本身
```
**原因**
- `right` 应该是 nums[i] 右侧的乘积
- 不应该包含 nums[i] 本身
- 必须先使用当前的 `right`,再更新
### 边界条件分析
**边界1数组长度为 1**
```
输入nums = [5]
输出:[1]
解释:
answer[0] = 1左侧为空
right = 1右侧为空
answer[0] = 1 × 1 = 1
```
**边界2包含 0**
```
输入nums = [1, 0, 3, 4]
过程:
answer = [1, 1, 0, 0] (左侧乘积)
i=3: answer[3] = 0 × 1 = 0, right = 4
i=2: answer[2] = 0 × 4 = 0, right = 12
i=1: answer[1] = 1 × 12 = 12, right = 0
i=0: answer[0] = 1 × 0 = 0, right = 1
输出:[0, 12, 0, 0]
验证:
answer[0] = 0 × 3 × 4 = 0 ✓
answer[1] = 1 × 3 × 4 = 12 ✓
answer[2] = 1 × 0 × 4 = 0 ✓
answer[3] = 1 × 0 × 3 = 0 ✓
```
**边界3包含多个 0**
```
输入nums = [0, 1, 0, 3]
过程:
answer = [1, 0, 0, 0] (左侧乘积)
i=3: answer[3] = 0 × 1 = 0, right = 3
i=2: answer[2] = 0 × 3 = 0, right = 0
i=1: answer[1] = 0 × 0 = 0, right = 0
i=0: answer[0] = 1 × 0 = 0, right = 0
输出:[0, 0, 0, 0]
验证:
任意位置都会乘到至少一个 0
```
**边界4负数**
```
输入nums = [-1, -2, -3, -4]
过程:
answer = [1, -1, 2, -6] (左侧乘积)
i=3: answer[3] = -6 × 1 = -6, right = -4
i=2: answer[2] = 2 × (-4) = -8, right = 12
i=1: answer[1] = -1 × 12 = -12, right = -24
i=0: answer[0] = 1 × (-24) = -24, right = 24
输出:[-24, -12, -8, -6]
验证:
answer[0] = (-2) × (-3) × (-4) = -24 ✓
answer[1] = (-1) × (-3) × (-4) = -12 ✓
answer[2] = (-1) × (-2) × (-4) = -8 ✓
answer[3] = (-1) × (-2) × (-3) = -6 ✓
```
### 复杂度分析(详细版)
**时间复杂度**
```
- 计算左侧乘积O(n),遍历一次
- 计算右侧乘积O(n),遍历一次
- 总计O(n) + O(n) = O(n)
为什么是 O(n)
- 两次线性扫描
- 每个元素只访问一次
- 常数操作(乘法和赋值)
```
**空间复杂度**
```
- 输出数组O(n),不计入额外空间
- 右侧变量O(1)
- 循环变量O(1)
- 总计O(1)(满足题目要求)
```
---
## 图解过程
```
nums = [1, 2, 3, 4]
步骤1计算左侧乘积
answer[0] = 1
answer[1] = answer[0] × nums[0] = 1 × 1 = 1
answer[2] = answer[1] × nums[1] = 1 × 2 = 2
answer[3] = answer[2] × nums[2] = 2 × 3 = 6
answer = [1, 1, 2, 6]
图示:
nums: [1, 2, 3, 4]
answer: [1, 1, 2, 6]
(×1)(×1)(×2)(×6) 左侧累积
步骤2计算右侧乘积并更新
right = 1
i=3: answer[3] = 6 × 1 = 6, right = 1 × 4 = 4
i=2: answer[2] = 2 × 4 = 8, right = 4 × 3 = 12
i=1: answer[1] = 1 × 12 = 12, right = 12 × 2 = 24
i=0: answer[0] = 1 × 24 = 24, right = 24 × 1 = 24
answer = [24, 12, 8, 6]
图示:
nums: [1, 2, 3, 4]
answer: [24, 12, 8, 6]
(×24)(×12)(×8)(×6) 右侧累积
最终结果:
answer[0] = 1 × (2 × 3 × 4) = 24
answer[1] = (1) × (3 × 4) = 12
answer[2] = (1 × 2) × (4) = 8
answer[3] = (1 × 2 × 3) × 1 = 6
```
---
## 代码实现
```go
func productExceptSelf(nums []int) []int {
n := len(nums)
answer := make([]int, n)
// 步骤1计算左侧乘积
answer[0] = 1
for i := 1; i < n; i++ {
answer[i] = answer[i-1] * nums[i-1]
}
// 步骤2计算右侧乘积并更新
right := 1
for i := n - 1; i >= 0; i-- {
answer[i] = answer[i] * right
right *= nums[i]
}
return answer
}
```
**关键点**
1. 用 `answer` 存储左侧乘积
2. 用 `right` 变量累积右侧乘积
3. 从右向左遍历,边计算边更新
---
## 执行过程演示
**输入**nums = [1, 2, 3, 4]
```
初始化answer = [0, 0, 0, 0], right = 1
步骤1计算左侧乘积
i=0: answer[0] = 1
i=1: answer[1] = answer[0] × nums[0] = 1 × 1 = 1
i=2: answer[2] = answer[1] × nums[1] = 1 × 2 = 2
i=3: answer[3] = answer[2] × nums[2] = 2 × 3 = 6
answer = [1, 1, 2, 6]
步骤2计算右侧乘积并更新
i=3: answer[3] = 6 × 1 = 6, right = 1 × 4 = 4
i=2: answer[2] = 2 × 4 = 8, right = 4 × 3 = 12
i=1: answer[1] = 1 × 12 = 12, right = 12 × 2 = 24
i=0: answer[0] = 1 × 24 = 24, right = 24 × 1 = 24
answer = [24, 12, 8, 6]
验证:
answer[0] = 2 × 3 × 4 = 24 ✓
answer[1] = 1 × 3 × 4 = 12 ✓
answer[2] = 1 × 2 × 4 = 8 ✓
answer[3] = 1 × 2 × 3 = 6 ✓
```
---
## 常见错误
### 错误1使用除法
❌ **错误代码**
```go
func productExceptSelf(nums []int) []int {
total := 1
for _, num := range nums {
total *= num
}
answer := make([]int, len(nums))
for i, num := range nums {
answer[i] = total / num // 错误!题目禁止除法
}
return answer
}
```
**问题**
1. 题目明确禁止使用除法
2. 如果 `num = 0`,除法会出错
3. 无法处理多个 0 的情况
---
### 错误2索引错误
❌ **错误代码**
```go
// 错误:包含了当前元素
answer[i] = answer[i-1] * nums[i] // 错误!
// 正确:不包含当前元素
answer[i] = answer[i-1] * nums[i-1] // 正确
```
**原因**
- `answer[i]` 是 nums[i] 左侧的乘积
- 不应该包含 nums[i] 本身
---
### 错误3更新顺序错误
❌ **错误代码**
```go
// 错误:先更新了 right
right *= nums[i]
answer[i] *= right // right 包含了 nums[i]
```
✅ **正确代码**
```go
// 正确:先使用 right再更新
answer[i] *= right
right *= nums[i]
```
**原因**
- `right` 应该是 nums[i] 右侧的乘积
- 不应该包含 nums[i] 本身
---
## 进阶问题
### Q1: 如果允许使用除法,如何处理 0
**思路**
1. 统计 0 的个数
2. 如果超过 1 个 0全部为 0
3. 如果正好 1 个 0只有该位置为总乘积其他为 0
4. 如果没有 0正常计算
```go
func productExceptSelfWithDivision(nums []int) []int {
n := len(nums)
answer := make([]int, n)
// 统计 0 的个数和总乘积
zeroCount := 0
totalProduct := 1
for _, num := range nums {
if num == 0 {
zeroCount++
} else {
totalProduct *= num
}
}
// 根据零的个数处理
for i, num := range nums {
if zeroCount > 1 {
answer[i] = 0
} else if zeroCount == 1 {
if num == 0 {
answer[i] = totalProduct
} else {
answer[i] = 0
}
} else {
answer[i] = totalProduct / num
}
}
return answer
}
```
---
### Q2: 如何处理大数溢出?
**思路**:使用对数转换
```go
func productExceptSelfLog(nums []int) []int {
n := len(nums)
logSum := make([]float64, n)
// 计算对数和
for i, num := range nums {
logSum[i] = math.Log(float64(num))
}
answer := make([]int, n)
totalLog := 0.0
for _, log := range logSum {
totalLog += log
}
// 转换回整数
for i, log := range logSum {
answer[i] = int(math.Exp(totalLog - log) + 0.5)
}
return answer
}
```
**注意**
- 对数转换会损失精度
- 适用于浮点数场景
- 整数场景需要其他方法
---
## P7 加分项
### 深度理解
- **分离思想**:将复杂问题分解为简单的子问题
- **空间优化**:利用输出数组,避免额外空间
- **递推关系**:左右乘积都可以递推计算
### 实战扩展
- **前缀和/后缀和**:类似思想,用加法代替乘法
- **范围查询**:线段树、树状数组
- **业务场景**:计算贡献度、权重分配
### 变形题目
1. [152. 乘积最大子数组](https://leetcode.cn/problems/maximum-product-subarray/)
2. 前缀和问题
3. 区间乘积查询
---
## 总结
**核心要点**
1. **分离思想**:将 answer[i] 分解为左侧 × 右侧
2. **递推计算**:左侧和右侧都可以递推计算
3. **空间优化**:用输出数组存储左侧,变量累积右侧
**易错点**
- 索引错误(是否包含当前元素)
- 更新顺序错误(先使用还是先更新)
- 边界条件0 的处理)
**最优解法**:分离左右乘积 + 空间优化,时间 O(n),空间 O(1)