按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
15 KiB
路径总和 (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
暴力解法分析
思路: 从根节点开始,递归计算到每个叶子节点的路径和
核心观察:
- 当前节点的路径和 = 节点值 + 子节点的路径和
- 到达叶子节点时,检查路径和是否等于目标值
- 这是一个天然的递归问题
递归思路:
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: 累加后比较(需要额外参数)
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 == nil但root.Right != nil: 不是叶子节点root.Right == nil但root.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)
}
总结
核心要点:
- 递归传递剩余目标值:
targetSum - node.Val - 叶子节点判断: 左右子节点都为空
- 空节点处理: 空树没有路径,返回false
- || 短路运算: 找到一条路径即可返回true
易错点:
- 判断顺序错误(空节点必须在叶子节点前)
- 忘记递减目标值
- 叶子节点判断不完整
- 空树与目标为0混淆
方法选择:
- 递归法(推荐):代码简洁,逻辑清晰
- BFS/DFS迭代:避免递归栈溢出
关键规律:
- 路径 = 根节点到叶子节点的节点序列
- 目标值递减传递,叶子节点判断是否为0
- 使用 || 找到任意一条满足条件的路径即可