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