力扣解题-112. 路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点 是指没有子节点的节点。
示例 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
解释:由于树是空的,所以不存在根节点到叶子节点的路径。
提示:
树中节点的数目在范围 [0, 5000] 内
-1000 <= Node.val <= 1000
-1000 <= targetSum <= 1000
Related Topics
树、深度优先搜索、广度优先搜索、二叉树
第一次解答
解题思路
核心方法:深度优先搜索(DFS)递归法,通过递归遍历二叉树的每条根到叶子的路径,逐步减去当前节点值,最终在叶子节点处校验剩余目标和是否等于节点值,逻辑简洁且符合二叉树的递归特性,是本题的基础最优解法。
核心逻辑拆解
判断路径总和的核心是“路径遍历+剩余和校验”,关键在于明确递归的终止条件和状态传递:
- 空树处理:若根节点
root == null,直接返回false(空树无任何路径); - 递归函数核心逻辑:
- 递归终止条件1:当前节点
root == null,返回false(路径未到叶子节点就结束,无效); - 递归终止条件2:当前节点是叶子节点(
root.left == null && root.right == null),校验targetSum == root.val(剩余目标和等于当前叶子节点值,说明路径和匹配); - 状态传递:递归遍历左、右子树,将目标和减去当前节点值(
targetSum - root.val)作为新的目标和传递下去; - 结果合并:只要左子树或右子树有任意一条路径满足条件,就返回
true(逻辑或||)。
- 递归终止条件1:当前节点
具体步骤(以示例1 root=[5,4,8,11,null,13,4,7,2,null,null,null,1]、targetSum=22为例)
| 递归层级 | 当前节点 | 剩余目标和 | 是否叶子节点 | 操作 | 结果 |
|---|---|---|---|---|---|
| 1 | 5 | 22 | 否 | 递归左子树(4),剩余17 | - |
| 2 | 4 | 17 | 否 | 递归左子树(11),剩余13 | - |
| 3 | 11 | 13 | 否 | 递归左子树(7)剩余2;递归右子树(2)剩余1 | - |
| 4 | 7 | 2 | 是 | 2≠7 → 返回false | - |
| 4 | 2 | 1 | 是 | 1≠2 → 返回false?(修正:实际剩余和计算为22-5-4-11=2,2==2 → 返回true) | true |
| 3 | 11 | 13 | 否 | 右子树返回true → 整体返回true | true |
最终递归回溯到根节点,返回true,与示例一致。 |
关键计算过程(示例1正确路径:5→4→11→2)
- 根节点5:剩余和=22-5=17;
- 节点4:剩余和=17-4=13;
- 节点11:剩余和=13-11=2;
- 叶子节点2:剩余和=2,等于节点值2 → 路径匹配,返回true。
性能说明
- 时间复杂度:O(n)(每个节点仅被访问一次,n为节点总数);
- 空间复杂度:O(h)(h为树的高度,递归栈深度等于树的高度):
- 最好情况(平衡二叉树):h = log₂n,空间复杂度O(logn);
- 最坏情况(斜树):h = n,空间复杂度O(n);
- 优势:
- 递归逻辑贴合路径遍历的直觉,易理解、易实现;
- 短路特性:只要找到一条匹配路径,递归会立即终止(逻辑或
||的短路求值),无需遍历所有节点; - 天然处理空树、单节点树等边界场景。
public boolean hasPathSum(TreeNode root, int targetSum) {
boolean result=false;
if(root!=null){
return dfs(root,targetSum);
}else {
return false;
}
}
public boolean dfs(TreeNode root,int targetSum){
if(root==null){
return false;
}
//说明是叶子节点,判断是否为0
if(root.left==null && root.right==null){
if(targetSum==root.val){
return true;
}else {
return false;
}
}
return dfs(root.left, targetSum - root.val) || dfs(root.right, targetSum - root.val);
}
示例解答
解题思路
解法1:广度优先搜索(BFS)迭代法(非递归)
核心方法:队列辅助层级遍历,使用两个队列分别存储待遍历的节点和对应的剩余目标和,逐层遍历二叉树,在叶子节点处校验剩余和是否匹配,避免递归栈开销,适合树高度较大的场景。
代码实现
import java.util.LinkedList;
import java.util.Queue;
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
// 队列1:存储待遍历的节点
Queue<TreeNode> nodeQueue = new LinkedList<>();
// 队列2:存储对应节点的剩余目标和
Queue<Integer> sumQueue = new LinkedList<>();
nodeQueue.offer(root);
sumQueue.offer(targetSum);
while (!nodeQueue.isEmpty()) {
TreeNode currNode = nodeQueue.poll();
int currSum = sumQueue.poll();
// 叶子节点校验
if (currNode.left == null && currNode.right == null) {
if (currSum == currNode.val) {
return true;
}
continue; // 非匹配叶子节点,跳过后续处理
}
// 左子节点入队
if (currNode.left != null) {
nodeQueue.offer(currNode.left);
sumQueue.offer(currSum - currNode.val);
}
// 右子节点入队
if (currNode.right != null) {
nodeQueue.offer(currNode.right);
sumQueue.offer(currSum - currNode.val);
}
}
// 遍历完所有节点未找到匹配路径
return false;
}
核心逻辑说明
- 初始化:根节点和初始目标和分别入队;
- 层级遍历:
- 取出队首节点和对应的剩余目标和;
- 若为叶子节点,校验剩余和是否等于节点值,匹配则直接返回
true; - 非叶子节点则将左右子节点入队,剩余和更新为
currSum - currNode.val;
- 终止条件:队列空(遍历完所有节点)则返回
false。
性能说明
- 时间复杂度:O(n)(每个节点仅入队/出队一次);
- 空间复杂度:O(n)(最坏情况队列存储一层所有节点,如完全二叉树最后一层有n/2个节点);
- 优势:
- 非递归实现,避免递归栈溢出风险(如斜树场景);
- 层级遍历逻辑清晰,可直观看到每一层的剩余和状态;
- 劣势:需要额外的队列空间,空间效率略低于递归法。
解法2:递归代码优化(精简版)
核心方法:在原递归逻辑基础上精简代码,合并冗余判断,保持逻辑等价但更简洁。
代码实现
public boolean hasPathSum(TreeNode root, int targetSum) {
// 空树直接返回false
if (root == null) {
return false;
}
// 叶子节点校验
if (root.left == null && root.right == null) {
return targetSum == root.val;
}
// 递归遍历左右子树,传递剩余和
return hasPathSum(root.left, targetSum - root.val)
|| hasPathSum(root.right, targetSum - root.val);
}
优势说明
- 逻辑等价:与原递归解法完全一致,仅精简了冗余的变量和判断;
- 代码更简洁:去掉了单独的
dfs函数,直接在主函数递归,减少代码量; - 可读性更高:核心逻辑一步到位,新手易理解。
解法3:迭代版DFS(栈模拟)
核心方法:栈辅助深度优先遍历,使用栈存储节点和对应的剩余目标和,模拟递归的DFS过程,非递归实现且保持与递归法相同的遍历顺序。
代码实现
import java.util.Stack;
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
// 栈:存储节点和对应的剩余目标和(使用数组或自定义类,此处用两个栈)
Stack<TreeNode> nodeStack = new Stack<>();
Stack<Integer> sumStack = new Stack<>();
nodeStack.push(root);
sumStack.push(targetSum);
while (!nodeStack.isEmpty()) {
TreeNode currNode = nodeStack.pop();
int currSum = sumStack.pop();
// 叶子节点校验
if (currNode.left == null && currNode.right == null) {
if (currSum == currNode.val) {
return true;
}
continue;
}
// 栈后进先出,先压右子树,后压左子树(保证DFS顺序)
if (currNode.right != null) {
nodeStack.push(currNode.right);
sumStack.push(currSum - currNode.val);
}
if (currNode.left != null) {
nodeStack.push(currNode.left);
sumStack.push(currSum - currNode.val);
}
}
return false;
}
核心逻辑说明
- 栈初始化:根节点和初始目标和入栈;
- DFS遍历:
- 弹出栈顶节点和剩余和,校验是否为叶子节点;
- 非叶子节点则先压右子树、后压左子树(栈后进先出,保证先遍历左子树);
- 终止条件:找到匹配路径返回
true,否则遍历完返回false。
性能说明
- 时间复杂度:O(n)(与递归法一致);
- 空间复杂度:O(h)(栈深度等于树的高度,与递归法一致);
- 优势:非递归实现,避免栈溢出,且空间效率与递归法相同。
总结
- DFS递归法(第一次解答):O(n)时间+O(h)空间,逻辑简洁、易理解,是本题的基础最优解法;
- BFS迭代法:O(n)时间+O(n)空间,非递归、层级遍历,适合树高度较大的场景;
- 递归精简版:代码更简洁,逻辑等价,可读性更高;
- DFS迭代版(栈模拟):O(n)时间+O(h)空间,非递归且空间效率与递归法一致;
- 关键技巧:
- 核心思想:路径总和的本质是“根到叶子的路径遍历+剩余和校验”;
- 终止条件:必须在叶子节点处校验剩余和,非叶子节点的空节点无意义;
- 短路优化:递归/迭代中使用逻辑或
||,找到匹配路径后立即返回,无需遍历所有节点。