按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
728 lines
15 KiB
Markdown
728 lines
15 KiB
Markdown
# 路径总和 (Path Sum)
|
||
|
||
LeetCode 112. 简单
|
||
|
||
## 题目描述
|
||
|
||
给你二叉树的根节点 `root` 和一个表示目标和的整数 `targetSum`,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和 `targetSum`。
|
||
|
||
**示例 1:**
|
||
```
|
||
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
|
||
输出:true
|
||
解释:等于目标和的根节点到叶节点路径如上图所示。
|
||
```
|
||
|
||
**示例 2:**
|
||
```
|
||
输入:root = [1,2,3], targetSum = 5
|
||
输出:false
|
||
解释:树中存在两条根节点到叶子节点的路径:
|
||
(1 --> 2): 和为 3
|
||
(1 --> 3): 和为 4
|
||
不存在 sum = 5 的根节点到叶子节点的路径。
|
||
```
|
||
|
||
**示例 3:**
|
||
```
|
||
输入:root = [], targetSum = 0
|
||
输出:false
|
||
解释:由于树是空的,所以不存在根节点到叶子节点的路径。
|
||
```
|
||
|
||
## 思路推导
|
||
|
||
### 理解路径总和
|
||
|
||
**路径定义**: 从根节点到叶子节点的节点序列
|
||
|
||
**叶子节点**: 没有子节点的节点(左右子节点都为空)
|
||
|
||
```
|
||
示例树:
|
||
5
|
||
/ \
|
||
4 8
|
||
/ / \
|
||
11 13 4
|
||
/ \ \
|
||
7 2 1
|
||
|
||
路径示例:
|
||
- 5 → 4 → 11 → 7: 和 = 27
|
||
- 5 → 4 → 11 → 2: 和 = 22 ✓
|
||
- 5 → 8 → 13: 和 = 26
|
||
- 5 → 8 → 4 → 1: 和 = 18
|
||
```
|
||
|
||
### 暴力解法分析
|
||
|
||
**思路**: 从根节点开始,递归计算到每个叶子节点的路径和
|
||
|
||
**核心观察**:
|
||
1. 当前节点的路径和 = 节点值 + 子节点的路径和
|
||
2. 到达叶子节点时,检查路径和是否等于目标值
|
||
3. 这是一个天然的递归问题
|
||
|
||
**递归思路**:
|
||
```
|
||
hasPathSum(node, target):
|
||
1. 空节点 → false(不存在路径)
|
||
2. 叶子节点 → 检查 node.Val == target
|
||
3. 非叶子节点 → 递归检查子节点
|
||
hasPathSum(left, target - node.Val) ||
|
||
hasPathSum(right, target - node.Val)
|
||
```
|
||
|
||
**为什么是 `target - node.Val`?**
|
||
- 到达当前节点时,已经累加了 `node.Val`
|
||
- 剩余需要的和 = `target - node.Val`
|
||
- 传递给子节点的目标是剩余值
|
||
|
||
**图解**:
|
||
```
|
||
目标: 22
|
||
|
||
5 target=22
|
||
/ \
|
||
4 8 target=17 (22-5)
|
||
/ / \
|
||
11 13 4 target=13 (17-4)
|
||
/ \ \
|
||
7 2 1 target=2 (13-11)
|
||
|
||
检查叶子节点:
|
||
- 7: 7 == 2? ✗
|
||
- 2: 2 == 2? ✓ 找到路径!
|
||
```
|
||
|
||
**时间复杂度**: O(n) - 最坏情况遍历所有节点
|
||
**空间复杂度**: O(h) - h为树高,递归栈空间
|
||
|
||
### 为什么这样思考?
|
||
|
||
**核心思想**:
|
||
1. **降维**: 从"寻找路径"降为"检查单个节点"
|
||
2. **递归传递**: 每层递归传递剩余目标值
|
||
3. **基准情况**: 叶子节点判断是否满足条件
|
||
|
||
**为什么不是累加后比较?**
|
||
```go
|
||
// 方案1: 累加后比较(需要额外参数)
|
||
func hasPathSum(node, target, currentSum int) bool {
|
||
if node == nil {
|
||
return false
|
||
}
|
||
currentSum += node.Val
|
||
if node.Left == nil && node.Right == nil {
|
||
return currentSum == target
|
||
}
|
||
return hasPathSum(node.Left, target, currentSum) ||
|
||
hasPathSum(node.Right, target, currentSum)
|
||
}
|
||
|
||
// 方案2: 递减目标值(更简洁)✓
|
||
func hasPathSum(node, target int) bool {
|
||
if node == nil {
|
||
return false
|
||
}
|
||
target -= node.Val
|
||
if node.Left == nil && node.Right == nil {
|
||
return target == 0
|
||
}
|
||
return hasPathSum(node.Left, target) ||
|
||
hasPathSum(node.Right, target)
|
||
}
|
||
```
|
||
|
||
**原因**: 方案2更简洁,不需要额外参数
|
||
|
||
## 解题思路
|
||
|
||
### 核心思想
|
||
从根节点开始,递归检查每条路径,每次递归传递剩余目标值,到达叶子节点时判断是否满足条件。
|
||
|
||
### 详细算法流程
|
||
|
||
**步骤1: 处理空节点**
|
||
```go
|
||
if root == nil {
|
||
return false // 空树没有路径
|
||
}
|
||
```
|
||
|
||
**关键点**: 这是递归的基准情况之一
|
||
|
||
**步骤2: 判断是否为叶子节点**
|
||
```go
|
||
if root.Left == nil && root.Right == nil {
|
||
return root.Val == targetSum
|
||
}
|
||
```
|
||
|
||
**关键点**: 叶子节点是路径的终点
|
||
|
||
**步骤3: 递归检查左右子树**
|
||
```go
|
||
// 传递剩余目标值
|
||
return hasPathSum(root.Left, targetSum - root.Val) ||
|
||
hasPathSum(root.Right, targetSum - root.Val)
|
||
```
|
||
|
||
**关键点**:
|
||
- `targetSum - root.Val` 是剩余目标值
|
||
- 使用 `||` 短路运算:找到一条路径即可返回true
|
||
|
||
**图解**:
|
||
```
|
||
示例: root = [1,2,3], targetSum = 3
|
||
|
||
1 target=3
|
||
/ \
|
||
2 3 target=2 (3-1)
|
||
|
||
检查:
|
||
├─ 路径1: 1 → 2
|
||
│ └─ 叶子节点2: 2 == 2? ✓ 返回true
|
||
└─ 路径2: 1 → 3
|
||
└─ 由于||短路,不再检查
|
||
|
||
最终返回: true
|
||
```
|
||
|
||
### 关键细节说明
|
||
|
||
**细节1: 为什么先判断空节点,再判断叶子节点?**
|
||
|
||
```go
|
||
// ❌ 错误顺序
|
||
if root.Left == nil && root.Right == nil {
|
||
return root.Val == targetSum
|
||
}
|
||
if root == nil {
|
||
return false
|
||
}
|
||
|
||
// ✅ 正确顺序
|
||
if root == nil {
|
||
return false
|
||
}
|
||
if root.Left == nil && root.Right == nil {
|
||
return root.Val == targetSum
|
||
}
|
||
```
|
||
|
||
**原因**: 空节点检查必须在前面,否则空节点会被误判
|
||
|
||
**细节2: 为什么叶子节点要同时检查左右为空?**
|
||
|
||
```go
|
||
// 只有左右都为空才是叶子节点
|
||
if root.Left == nil && root.Right == nil {
|
||
return root.Val == targetSum
|
||
}
|
||
```
|
||
|
||
**原因**:
|
||
- `root.Left == nil` 但 `root.Right != nil`: 不是叶子节点
|
||
- `root.Right == nil` 但 `root.Left != nil`: 不是叶子节点
|
||
- 只有两者都为空才是叶子节点
|
||
|
||
**细节3: 为什么使用 `||` 而不是 `&&`?**
|
||
|
||
```go
|
||
return hasPathSum(root.Left, targetSum - root.Val) ||
|
||
hasPathSum(root.Right, targetSum - root.Val)
|
||
```
|
||
|
||
**原因**:
|
||
- `||`: 只要有一条路径满足即可
|
||
- `&&`: 需要所有路径都满足(错误理解)
|
||
|
||
### 边界条件分析
|
||
|
||
**边界1: 空树**
|
||
```
|
||
输入: root = nil, targetSum = 0
|
||
输出: false
|
||
处理: 直接返回false(空树没有路径)
|
||
```
|
||
|
||
**边界2: 只有根节点且值等于目标**
|
||
```
|
||
输入: root = [1], targetSum = 1
|
||
输出: true
|
||
处理:
|
||
- 1是叶子节点
|
||
- 1 == 1? ✓ 返回true
|
||
```
|
||
|
||
**边界3: 只有根节点但值不等于目标**
|
||
```
|
||
输入: root = [1], targetSum = 2
|
||
输出: false
|
||
处理:
|
||
- 1是叶子节点
|
||
- 1 == 2? ✗ 返回false
|
||
```
|
||
|
||
**边界4: 目标值为0但树非空**
|
||
```
|
||
输入:
|
||
1
|
||
/ \
|
||
2 3
|
||
targetSum = 0
|
||
|
||
输出: false
|
||
处理:
|
||
- 路径1: 1 → 2, 和=3 ≠ 0
|
||
- 路径2: 1 → 3, 和=4 ≠ 0
|
||
- 返回false
|
||
```
|
||
|
||
**边界5: 负数节点值**
|
||
```
|
||
输入:
|
||
-2
|
||
/ \
|
||
3 1
|
||
targetSum = 1
|
||
|
||
输出: true
|
||
处理:
|
||
- 路径1: -2 → 3, 和=1 ✓
|
||
- 路径2: -2 → 1, 和=-1 ✗
|
||
- 返回true
|
||
```
|
||
|
||
**边界6: 单链树**
|
||
```
|
||
输入:
|
||
1
|
||
/
|
||
2
|
||
/
|
||
3
|
||
targetSum = 6
|
||
|
||
输出: true
|
||
处理:
|
||
- 路径: 1 → 2 → 3, 和=6 ✓
|
||
- 返回true
|
||
```
|
||
|
||
### 复杂度分析(详细版)
|
||
|
||
**时间复杂度**:
|
||
```
|
||
- 最坏情况: O(n) - 遍历所有节点
|
||
- 最好情况: O(1) - 根节点就是目标
|
||
- 平均情况: O(n)
|
||
|
||
为什么是O(n)?
|
||
- 每个节点最多访问一次
|
||
- 找到路径后可能提前退出(||短路)
|
||
- 最坏情况需要遍历所有节点
|
||
```
|
||
|
||
**空间复杂度**:
|
||
```
|
||
- 递归栈: O(h) - h为树高
|
||
- 最坏情况(链状树): O(n)
|
||
- 最好情况(完全平衡树): O(log n)
|
||
- 总计: O(h)
|
||
```
|
||
|
||
### 执行过程演示
|
||
|
||
**输入**:
|
||
```
|
||
5
|
||
/ \
|
||
4 8
|
||
/ / \
|
||
11 13 4
|
||
/ \ \
|
||
7 2 1
|
||
targetSum = 22
|
||
```
|
||
|
||
**执行过程**:
|
||
```
|
||
调用 hasPathSum(5, 22):
|
||
├─ target = 22 - 5 = 17
|
||
├─ 5不是叶子节点
|
||
├─ 调用 hasPathSum(4, 17):
|
||
│ ├─ target = 17 - 4 = 13
|
||
│ ├─ 4不是叶子节点
|
||
│ ├─ 调用 hasPathSum(11, 13):
|
||
│ │ ├─ target = 13 - 11 = 2
|
||
│ │ ├─ 11不是叶子节点
|
||
│ │ ├─ 调用 hasPathSum(7, 2):
|
||
│ │ │ ├─ target = 2 - 7 = -5
|
||
│ │ │ ├─ 7是叶子节点
|
||
│ │ │ └─ -5 == 7? ✗ 返回false
|
||
│ │ ├─ 调用 hasPathSum(2, 2):
|
||
│ │ │ ├─ target = 2 - 2 = 0
|
||
│ │ │ ├─ 2是叶子节点
|
||
│ │ │ └─ 0 == 2? ✓ 返回true
|
||
│ │ └─ 返回 true || false = true
|
||
│ └─ 返回 true(短路,不检查8)
|
||
└─ 返回 true
|
||
|
||
最终返回: true
|
||
```
|
||
|
||
**找到的路径**: 5 → 4 → 11 → 2,和 = 22
|
||
|
||
## 代码实现
|
||
|
||
### 方法一:递归(推荐)
|
||
|
||
```go
|
||
func hasPathSum(root *TreeNode, targetSum int) bool {
|
||
if root == nil {
|
||
return false
|
||
}
|
||
|
||
// 叶子节点,检查是否满足条件
|
||
if root.Left == nil && root.Right == nil {
|
||
return root.Val == targetSum
|
||
}
|
||
|
||
// 递归检查左右子树
|
||
return hasPathSum(root.Left, targetSum - root.Val) ||
|
||
hasPathSum(root.Right, targetSum - root.Val)
|
||
}
|
||
```
|
||
|
||
**复杂度**: O(n) 时间,O(h) 空间
|
||
|
||
### 方法二:递归(提前返回优化)
|
||
|
||
```go
|
||
func hasPathSum(root *TreeNode, targetSum int) bool {
|
||
if root == nil {
|
||
return false
|
||
}
|
||
|
||
targetSum -= root.Val
|
||
|
||
// 叶子节点
|
||
if root.Left == nil && root.Right == nil {
|
||
return targetSum == 0
|
||
}
|
||
|
||
// 先检查左子树,找到路径直接返回
|
||
if root.Left != nil {
|
||
if hasPathSum(root.Left, targetSum) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// 再检查右子树
|
||
if root.Right != nil {
|
||
if hasPathSum(root.Right, targetSum) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
```
|
||
|
||
**复杂度**: O(n) 时间,O(h) 空间
|
||
|
||
### 方法三:BFS(队列)
|
||
|
||
```go
|
||
func hasPathSum(root *TreeNode, targetSum int) bool {
|
||
if root == nil {
|
||
return false
|
||
}
|
||
|
||
type NodeWithSum struct {
|
||
node *TreeNode
|
||
sum int
|
||
}
|
||
|
||
queue := []NodeWithSum{{root, 0}}
|
||
|
||
for len(queue) > 0 {
|
||
current := queue[0]
|
||
queue = queue[1:]
|
||
|
||
current.sum += current.node.Val
|
||
|
||
// 叶子节点
|
||
if current.node.Left == nil && current.node.Right == nil {
|
||
if current.sum == targetSum {
|
||
return true
|
||
}
|
||
continue
|
||
}
|
||
|
||
if current.node.Left != nil {
|
||
queue = append(queue, NodeWithSum{current.node.Left, current.sum})
|
||
}
|
||
if current.node.Right != nil {
|
||
queue = append(queue, NodeWithSum{current.node.Right, current.sum})
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
```
|
||
|
||
**复杂度**: O(n) 时间,O(n) 空间
|
||
|
||
### 方法四:DFS迭代(栈)
|
||
|
||
```go
|
||
func hasPathSum(root *TreeNode, targetSum int) bool {
|
||
if root == nil {
|
||
return false
|
||
}
|
||
|
||
type NodeWithSum struct {
|
||
node *TreeNode
|
||
sum int
|
||
}
|
||
|
||
stack := []NodeWithSum{{root, 0}}
|
||
|
||
for len(stack) > 0 {
|
||
current := stack[len(stack)-1]
|
||
stack = stack[:len(stack)-1]
|
||
|
||
current.sum += current.node.Val
|
||
|
||
// 叶子节点
|
||
if current.node.Left == nil && current.node.Right == nil {
|
||
if current.sum == targetSum {
|
||
return true
|
||
}
|
||
continue
|
||
}
|
||
|
||
if current.node.Right != nil {
|
||
stack = append(stack, NodeWithSum{current.node.Right, current.sum})
|
||
}
|
||
if current.node.Left != nil {
|
||
stack = append(stack, NodeWithSum{current.node.Left, current.sum})
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
```
|
||
|
||
**复杂度**: O(n) 时间,O(n) 空间
|
||
|
||
## 常见错误
|
||
|
||
### 错误1: 判断顺序错误
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
func hasPathSum(root *TreeNode, targetSum int) bool {
|
||
if root.Left == nil && root.Right == nil {
|
||
return root.Val == targetSum
|
||
}
|
||
if root == nil {
|
||
return false // 永远不会执行!
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
func hasPathSum(root *TreeNode, targetSum int) bool {
|
||
if root == nil {
|
||
return false
|
||
}
|
||
if root.Left == nil && root.Right == nil {
|
||
return root.Val == targetSum
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**原因**: 空节点检查必须在前面
|
||
|
||
### 错误2: 忘记递减目标值
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
return hasPathSum(root.Left, targetSum) || // 忘记减去root.Val
|
||
hasPathSum(root.Right, targetSum)
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
return hasPathSum(root.Left, targetSum - root.Val) ||
|
||
hasPathSum(root.Right, targetSum - root.Val)
|
||
```
|
||
|
||
**原因**: 需要传递剩余目标值
|
||
|
||
### 错误3: 叶子节点判断错误
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
if root.Left == nil { // 只检查左子树
|
||
return root.Val == targetSum
|
||
}
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
if root.Left == nil && root.Right == nil { // 同时检查左右
|
||
return root.Val == targetSum
|
||
}
|
||
```
|
||
|
||
**原因**: 叶子节点必须左右子节点都为空
|
||
|
||
### 错误4: 空树与目标为0混淆
|
||
|
||
❌ **错误理解**:
|
||
```go
|
||
// 错误:认为空树且目标为0时返回true
|
||
if root == nil && targetSum == 0 {
|
||
return true
|
||
}
|
||
```
|
||
|
||
✅ **正确理解**:
|
||
```go
|
||
// 空树没有路径,永远返回false
|
||
if root == nil {
|
||
return false
|
||
}
|
||
```
|
||
|
||
**原因**: 空树不存在根节点到叶子节点的路径
|
||
|
||
## 变体问题
|
||
|
||
### 变体1: 路径总和 II(返回所有路径)
|
||
|
||
```go
|
||
func pathSum(root *TreeNode, targetSum int) [][]int {
|
||
result := [][]int{}
|
||
path := []int{}
|
||
dfs(root, targetSum, path, &result)
|
||
return result
|
||
}
|
||
|
||
func dfs(node *TreeNode, target int, path []int, result *[][]int) {
|
||
if node == nil {
|
||
return
|
||
}
|
||
|
||
target -= node.Val
|
||
path = append(path, node.Val)
|
||
|
||
if node.Left == nil && node.Right == nil {
|
||
if target == 0 {
|
||
// 复制path,避免修改
|
||
temp := make([]int, len(path))
|
||
copy(temp, path)
|
||
*result = append(*result, temp)
|
||
}
|
||
return
|
||
}
|
||
|
||
dfs(node.Left, target, path, result)
|
||
dfs(node.Right, target, path, result)
|
||
}
|
||
```
|
||
|
||
### 变体2: 路径总和 III(任意起点和终点)
|
||
|
||
```go
|
||
func pathSumIII(root *TreeNode, targetSum int) int {
|
||
if root == nil {
|
||
return 0
|
||
}
|
||
|
||
// 以当前节点为起点的路径数
|
||
count := countPath(root, targetSum)
|
||
|
||
// 递归检查左右子树
|
||
count += pathSumIII(root.Left, targetSum)
|
||
count += pathSumIII(root.Right, targetSum)
|
||
|
||
return count
|
||
}
|
||
|
||
func countPath(node *TreeNode, target int) int {
|
||
if node == nil {
|
||
return 0
|
||
}
|
||
|
||
count := 0
|
||
if node.Val == target {
|
||
count++
|
||
}
|
||
|
||
count += countPath(node.Left, target - node.Val)
|
||
count += countPath(node.Right, target - node.Val)
|
||
|
||
return count
|
||
}
|
||
```
|
||
|
||
### 变体3: 最大路径和(路径可以任意起止)
|
||
|
||
```go
|
||
func maxPathSum(root *TreeNode) int {
|
||
maxSum := math.MinInt32
|
||
maxGain(root, &maxSum)
|
||
return maxSum
|
||
}
|
||
|
||
func maxGain(node *TreeNode, maxSum *int) int {
|
||
if node == nil {
|
||
return 0
|
||
}
|
||
|
||
// 递归计算左右子树的最大贡献值
|
||
leftGain := max(0, maxGain(node.Left, maxSum))
|
||
rightGain := max(0, maxGain(node.Right, maxSum))
|
||
|
||
// 当前节点的最大路径和
|
||
currentSum := node.Val + leftGain + rightGain
|
||
*maxSum = max(*maxSum, currentSum)
|
||
|
||
// 返回当前节点的最大贡献值
|
||
return node.Val + max(leftGain, rightGain)
|
||
}
|
||
```
|
||
|
||
## 总结
|
||
|
||
**核心要点**:
|
||
1. **递归传递剩余目标值**: `targetSum - node.Val`
|
||
2. **叶子节点判断**: 左右子节点都为空
|
||
3. **空节点处理**: 空树没有路径,返回false
|
||
4. **|| 短路运算**: 找到一条路径即可返回true
|
||
|
||
**易错点**:
|
||
- 判断顺序错误(空节点必须在叶子节点前)
|
||
- 忘记递减目标值
|
||
- 叶子节点判断不完整
|
||
- 空树与目标为0混淆
|
||
|
||
**方法选择**:
|
||
- 递归法(推荐):代码简洁,逻辑清晰
|
||
- BFS/DFS迭代:避免递归栈溢出
|
||
|
||
**关键规律**:
|
||
- 路径 = 根节点到叶子节点的节点序列
|
||
- 目标值递减传递,叶子节点判断是否为0
|
||
- 使用 || 找到任意一条满足条件的路径即可
|