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

728 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.
# 路径总和 (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
- 使用 || 找到任意一条满足条件的路径即可