docs: 改进LeetCode二叉树题目解题思路

按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 21:33:57 +08:00
parent 67189941d8
commit 5c1c974e88
14 changed files with 7817 additions and 139 deletions

View File

@@ -1,37 +1,689 @@
# 除自身以外数组的乘积 (Product of Array Except Self)
LeetCode 238. Medium
## 题目描述
给你一个整数数组 nums返回数组 answer其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
给你一个整数数组 `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]` 分解为左侧乘积和右侧乘积的乘积
## Go 代码
**为什么这样思考?**
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
}
```
**复杂度:** O(n) 时间O(1) 额外空间(不包括输出数组)
**关键点**
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)