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

15 KiB
Raw Blame History

路径总和 (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. 基准情况: 叶子节点判断是否满足条件

为什么不是累加后比较?

// 方案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: 处理空节点

if root == nil {
    return false  // 空树没有路径
}

关键点: 这是递归的基准情况之一

步骤2: 判断是否为叶子节点

if root.Left == nil && root.Right == nil {
    return root.Val == targetSum
}

关键点: 叶子节点是路径的终点

步骤3: 递归检查左右子树

// 传递剩余目标值
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: 为什么先判断空节点,再判断叶子节点?

// ❌ 错误顺序
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: 为什么叶子节点要同时检查左右为空?

// 只有左右都为空才是叶子节点
if root.Left == nil && root.Right == nil {
    return root.Val == targetSum
}

原因:

  • root.Left == nilroot.Right != nil: 不是叶子节点
  • root.Right == nilroot.Left != nil: 不是叶子节点
  • 只有两者都为空才是叶子节点

细节3: 为什么使用 || 而不是 &&?

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

代码实现

方法一:递归(推荐)

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) 空间

方法二:递归(提前返回优化)

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队列

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迭代

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: 判断顺序错误

错误写法:

func hasPathSum(root *TreeNode, targetSum int) bool {
    if root.Left == nil && root.Right == nil {
        return root.Val == targetSum
    }
    if root == nil {
        return false  // 永远不会执行!
    }
    // ...
}

正确写法:

func hasPathSum(root *TreeNode, targetSum int) bool {
    if root == nil {
        return false
    }
    if root.Left == nil && root.Right == nil {
        return root.Val == targetSum
    }
    // ...
}

原因: 空节点检查必须在前面

错误2: 忘记递减目标值

错误写法:

return hasPathSum(root.Left, targetSum) ||  // 忘记减去root.Val
       hasPathSum(root.Right, targetSum)

正确写法:

return hasPathSum(root.Left, targetSum - root.Val) ||
       hasPathSum(root.Right, targetSum - root.Val)

原因: 需要传递剩余目标值

错误3: 叶子节点判断错误

错误写法:

if root.Left == nil {  // 只检查左子树
    return root.Val == targetSum
}

正确写法:

if root.Left == nil && root.Right == nil {  // 同时检查左右
    return root.Val == targetSum
}

原因: 叶子节点必须左右子节点都为空

错误4: 空树与目标为0混淆

错误理解:

// 错误认为空树且目标为0时返回true
if root == nil && targetSum == 0 {
    return true
}

正确理解:

// 空树没有路径永远返回false
if root == nil {
    return false
}

原因: 空树不存在根节点到叶子节点的路径

变体问题

变体1: 路径总和 II返回所有路径

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任意起点和终点

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: 最大路径和(路径可以任意起止)

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
  • 使用 || 找到任意一条满足条件的路径即可