LeetCode 第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
解题思路
方法一:递归(深度优先搜索)
这道题可以使用递归的方式,从根节点开始,每次递归时减去当前节点的值,直到达到叶子节点时检查剩余的目标和是否为0。
关键点:
- 递归终止条件:当前节点为空,返回false
- 如果当前节点是叶子节点,检查剩余目标和是否等于当前节点值
- 对于非叶子节点,递归检查左右子树是否存在满足条件的路径
具体步骤:
- 如果根节点为空,返回false
- 如果当前节点是叶子节点(左右子节点都为空),检查targetSum是否等于当前节点的值
- 递归检查左子树,目标和减去当前节点的值
- 递归检查右子树,目标和减去当前节点的值
- 如果左子树或右子树返回true,则返回true,否则返回false
时间复杂度:O(n),其中n是树中节点的数量,最坏情况下需要遍历所有节点 空间复杂度:O(h),其中h是树的高度,递归调用栈的深度
方法二:迭代(广度优先搜索)
我们也可以使用广度优先搜索(BFS)来解决这个问题。使用队列同时存储节点和从根节点到当前节点的路径和。
关键点:
- 使用队列存储节点和对应的路径和
- 当遇到叶子节点时,检查路径和是否等于目标和
- 使用BFS遍历整棵树
具体步骤:
- 如果根节点为空,返回false
- 初始化队列,将根节点和根节点的值入队
- 当队列不为空时,执行以下操作: a. 出队一个节点和对应的路径和 b. 如果当前节点是叶子节点,检查路径和是否等于目标和,如果是,返回true c. 如果当前节点有左子节点,将左子节点和更新后的路径和入队 d. 如果当前节点有右子节点,将右子节点和更新后的路径和入队
- 如果遍历完整棵树仍未找到满足条件的路径,返回false
时间复杂度:O(n),最坏情况下需要遍历所有节点 空间复杂度:O(n),队列中最多存储n个节点
图解思路
方法一:递归过程分析表
以示例1为例,targetSum = 22
| 节点 | 当前值 | 剩余目标和 | 是否叶子节点 | 结果 | 说明 |
|---|---|---|---|---|---|
| 5 | 5 | 22 | 否 | - | 递归检查左右子树 |
| 4 | 4 | 17 | 否 | - | 递归检查左子树 |
| 11 | 11 | 13 | 否 | - | 递归检查左右子树 |
| 7 | 7 | 6 | 是 | false | 7 != 6 |
| 2 | 2 | 11 | 是 | true | 2 == 2 |
| 8 | 8 | 17 | 否 | - | 递归检查左右子树 |
| 13 | 13 | 9 | 是 | false | 13 != 9 |
| 4 | 4 | 9 | 否 | - | 递归检查右子树 |
| 1 | 1 | 5 | 是 | false | 1 != 5 |
方法二:BFS遍历过程表
以示例1为例,targetSum = 22
| 队列状态 | 当前节点 | 当前路径和 | 是否叶子节点 | 结果 | 说明 |
|---|---|---|---|---|---|
| [(5,5)] | 5 | 5 | 否 | - | 将左右子节点入队 |
| [(4,9), (8,13)] | 4 | 9 | 否 | - | 将左子节点入队 |
| [(8,13), (11,20)] | 8 | 13 | 否 | - | 将左右子节点入队 |
| [(11,20), (13,26), (4,17)] | 11 | 20 | 否 | - | 将左右子节点入队 |
| [(13,26), (4,17), (7,27), (2,22)] | 13 | 26 | 是 | false | 26 != 22 |
| [(4,17), (7,27), (2,22)] | 4 | 17 | 否 | - | 将右子节点入队 |
| [(7,27), (2,22), (1,18)] | 7 | 27 | 是 | false | 27 != 22 |
| [(2,22), (1,18)] | 2 | 22 | 是 | true | 22 == 22,返回true |
代码实现
C# 实现
/**
* Definition for a binary tree node.
* public class TreeNode {
* public int val;
* public TreeNode left;
* public TreeNode right;
* public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
public class Solution {
// 方法一:递归(深度优先搜索)
public bool 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);
}
// 方法二:迭代(广度优先搜索)
public bool HasPathSumBFS(TreeNode root, int targetSum) {
// 如果根节点为空,返回false
if (root == null) {
return false;
}
// 使用队列存储节点和对应的路径和
Queue<(TreeNode node, int sum)> queue = new Queue<(TreeNode, int)>();
queue.Enqueue((root, root.val));
while (queue.Count > 0) {
var (node, pathSum) = queue.Dequeue();
// 如果是叶子节点,检查路径和是否等于目标和
if (node.left == null && node.right == null && pathSum == targetSum) {
return true;
}
// 将左右子节点和更新后的路径和入队
if (node.left != null) {
queue.Enqueue((node.left, pathSum + node.left.val));
}
if (node.right != null) {
queue.Enqueue((node.right, pathSum + node.right.val));
}
}
return false;
}
}
Python 实现
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
# 方法一:递归(深度优先搜索)
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
# 如果根节点为空,返回False
if not root:
return False
# 如果是叶子节点,检查剩余目标和是否等于当前节点值
if not root.left and not root.right:
return targetSum == root.val
# 递归检查左右子树
return (self.hasPathSum(root.left, targetSum - root.val) or
self.hasPathSum(root.right, targetSum - root.val))
# 方法二:迭代(广度优先搜索)
def hasPathSumBFS(self, root: Optional[TreeNode], targetSum: int) -> bool:
# 如果根节点为空,返回False
if not root:
return False
# 使用队列存储节点和对应的路径和
queue = collections.deque([(root, root.val)])
while queue:
node, path_sum = queue.popleft()
# 如果是叶子节点,检查路径和是否等于目标和
if not node.left and not node.right and path_sum == targetSum:
return True
# 将左右子节点和更新后的路径和入队
if node.left:
queue.append((node.left, path_sum + node.left.val))
if node.right:
queue.append((node.right, path_sum + node.right.val))
return False
C++ 实现
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 方法一:递归(深度优先搜索)
bool hasPathSum(TreeNode* root, int targetSum) {
// 如果根节点为空,返回false
if (root == nullptr) {
return false;
}
// 如果是叶子节点,检查剩余目标和是否等于当前节点值
if (root->left == nullptr && root->right == nullptr) {
return targetSum == root->val;
}
// 递归检查左右子树
return hasPathSum(root->left, targetSum - root->val) ||
hasPathSum(root->right, targetSum - root->val);
}
// 方法二:迭代(广度优先搜索)
bool hasPathSumBFS(TreeNode* root, int targetSum) {
// 如果根节点为空,返回false
if (root == nullptr) {
return false;
}
// 使用队列存储节点和对应的路径和
queue<pair<TreeNode*, int>> q;
q.push({root, root->val});
while (!q.empty()) {
TreeNode* node = q.front().first;
int pathSum = q.front().second;
q.pop();
// 如果是叶子节点,检查路径和是否等于目标和
if (node->left == nullptr && node->right == nullptr && pathSum == targetSum) {
return true;
}
// 将左右子节点和更新后的路径和入队
if (node->left != nullptr) {
q.push({node->left, pathSum + node->left->val});
}
if (node->right != nullptr) {
q.push({node->right, pathSum + node->right->val});
}
}
return false;
}
};
执行结果
C# 实现
- 执行用时:104 ms
- 内存消耗:41.3 MB
Python 实现
- 执行用时:40 ms
- 内存消耗:17.8 MB
C++ 实现
- 执行用时:8 ms
- 内存消耗:21.2 MB
性能对比
| 语言 | 执行用时 | 内存消耗 | 特点 |
|---|---|---|---|
| C# | 104 ms | 41.3 MB | 代码结构清晰,性能适中 |
| Python | 40 ms | 17.8 MB | 代码简洁,性能良好 |
| C++ | 8 ms | 21.2 MB | 执行速度最快,内存占用适中 |
代码亮点
- 🎯 递归解法简洁明了,直接反映了问题的本质
- 💡 BFS解法通过同时存储节点和路径和,避免了重复计算
- 🔍 正确处理了叶子节点的定义和空树的边界情况
- 🎨 两种解法各有优势,递归代码简洁,迭代避免栈溢出风险
常见错误分析
- 🚫 忽略了题目要求必须是从根节点到叶子节点的路径
- 🚫 没有正确处理空树的情况
- 🚫 错误地认为任何节点都可以作为路径的终点
- 🚫 在递归过程中没有正确更新目标和
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 递归(DFS) | O(n) | O(h) | 代码简洁,直观易懂 | 对于高度较大的树可能导致栈溢出 |
| 迭代(BFS) | O(n) | O(n) | 避免栈溢出,适用于所有树 | 代码稍复杂,需要额外空间存储路径和 |