LeetCode 112. 路径总和
摘要
LeetCode 112 路径总和题解,使用前序 DFS 递归,通过递减目标值判断路径是否存在,利用逻辑或实现快速短路返回,时间复杂度 O(n),空间复杂度 O(n)(递归栈)。
标签
#算法 #LeetCode #二叉树 #DFS #递归 #题解
目录
-
题目描述
-
易错点
-
思路
- 为什么使用前序 DFS
- 减法思想的优雅之处
- 逻辑或的快速回调
-
编写代码
1. 题目描述
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum。
叶子节点 是指没有子节点的节点。
示例 1:
text
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径为 5->4->11->2。
示例 2:
text
输入:root = [1,2,3], targetSum = 5
输出:false
解释:不存在根节点到叶子节点的路径和为 5。
示例 3:
text
输入:root = [], targetSum = 0
输出:false
提示:
- 树中节点的数目在范围
[0, 5000]内 -1000 <= Node.val <= 1000-1000 <= targetSum <= 1000
2. 易错点
- 空树直接返回
false(即使 targetSum 为 0)。 - 路径必须是从根节点到叶子节点,不能提前在非叶子节点结束。
- 判断条件应为:当前节点是叶子且节点值等于剩余目标值。
- 递归时,将 targetSum 减去当前节点值传递给子节点,而不是累加路径和。
- 逻辑或的短路特性:一旦左子树返回 true,右子树将不再执行,需确保这种快速返回不会影响结果(因为只需存在一条路径)。
3. 思路
为什么使用前序 DFS
本题需要从根节点出发,沿着路径向下累加(或递减)直到叶子。处理顺序必然是:先处理当前节点,再递归处理其子节点。这正是前序遍历(根-左-右)的特征。
若使用中序或后序,则需要额外保存路径状态,或难以在遍历到叶子时判断是否满足条件。前序 DFS 天然适配:每到一个节点,立即将当前节点值纳入路径计算,然后向下传递,符合人类思维习惯。
递归结构直接对应前序:先判断当前节点,再递归左、右子树。代码简洁且易理解。
减法思想的优雅之处
常见的思路是维护一个当前路径和 sum,每次累加节点值,到叶子时比较 sum == targetSum。这需要额外传递累计值。
减法思想将目标值作为“剩余需要补齐的和”向下传递:
- 从根开始,目标为
targetSum。 - 进入子节点时,目标变为
targetSum - node.val。 - 到叶子节点时,只需检查
node.val == remaining,等价于路径和等于原始目标。
这种做法的优点:
- 无需额外维护累计变量,减少参数个数。
- 直观反映“还差多少”的含义,更贴近问题本质。
- 递归终止条件清晰:叶子节点时只需一次比较。
逻辑或的快速回调
代码中 return dfs(left, ...) || dfs(right, ...) 利用了 C 语言逻辑或的短路求值特性:
- 先计算左子树递归,若返回
true,则整个表达式立即为true,右子树递归根本不会执行。 - 若左子树返回
false,才继续计算右子树。
这带来的好处:
- 一旦找到一条有效路径,立即停止搜索,避免不必要的遍历,提升效率。
- 代码简洁,一行完成两个子树的探索与结果合并。
注意:短路求值仅在操作数为布尔类型时生效,这里正是 bool 类型,完全适用。
时间复杂度:最坏情况下遍历所有节点 O(n),但短路特性可能提前终止,平均更快。空间复杂度:递归栈深度 O(h)。
4. 编写代码
c 运行
c
bool hasPathSum(struct TreeNode* root, int targetSum) {
if(!root) return false;
if(!root->right && !root->left && root->val == targetSum)
return true;
return hasPathSum(root->left, targetSum - root->val) || hasPathSum(root->right, targetSum - root->val);
}