LeetCode 第112题:路径总和

77 阅读8分钟

LeetCode 第112题:路径总和

题目描述

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false

叶子节点 是指没有子节点的节点。

难度

简单

题目链接

点击在LeetCode中查看题目

示例

示例 1:

示例1

输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。

示例 2:

示例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
  • 如果当前节点是叶子节点,检查剩余目标和是否等于当前节点值
  • 对于非叶子节点,递归检查左右子树是否存在满足条件的路径

具体步骤:

  1. 如果根节点为空,返回false
  2. 如果当前节点是叶子节点(左右子节点都为空),检查targetSum是否等于当前节点的值
  3. 递归检查左子树,目标和减去当前节点的值
  4. 递归检查右子树,目标和减去当前节点的值
  5. 如果左子树或右子树返回true,则返回true,否则返回false

时间复杂度:O(n),其中n是树中节点的数量,最坏情况下需要遍历所有节点 空间复杂度:O(h),其中h是树的高度,递归调用栈的深度

方法二:迭代(广度优先搜索)

我们也可以使用广度优先搜索(BFS)来解决这个问题。使用队列同时存储节点和从根节点到当前节点的路径和。

关键点:

  • 使用队列存储节点和对应的路径和
  • 当遇到叶子节点时,检查路径和是否等于目标和
  • 使用BFS遍历整棵树

具体步骤:

  1. 如果根节点为空,返回false
  2. 初始化队列,将根节点和根节点的值入队
  3. 当队列不为空时,执行以下操作: a. 出队一个节点和对应的路径和 b. 如果当前节点是叶子节点,检查路径和是否等于目标和,如果是,返回true c. 如果当前节点有左子节点,将左子节点和更新后的路径和入队 d. 如果当前节点有右子节点,将右子节点和更新后的路径和入队
  4. 如果遍历完整棵树仍未找到满足条件的路径,返回false

时间复杂度:O(n),最坏情况下需要遍历所有节点 空间复杂度:O(n),队列中最多存储n个节点

图解思路

方法一:递归过程分析表

以示例1为例,targetSum = 22

节点当前值剩余目标和是否叶子节点结果说明
5522-递归检查左右子树
4417-递归检查左子树
111113-递归检查左右子树
776false7 != 6
2211true2 == 2
8817-递归检查左右子树
13139false13 != 9
449-递归检查右子树
115false1 != 5

方法二:BFS遍历过程表

以示例1为例,targetSum = 22

队列状态当前节点当前路径和是否叶子节点结果说明
[(5,5)]55-将左右子节点入队
[(4,9), (8,13)]49-将左子节点入队
[(8,13), (11,20)]813-将左右子节点入队
[(11,20), (13,26), (4,17)]1120-将左右子节点入队
[(13,26), (4,17), (7,27), (2,22)]1326false26 != 22
[(4,17), (7,27), (2,22)]417-将右子节点入队
[(7,27), (2,22), (1,18)]727false27 != 22
[(2,22), (1,18)]222true22 == 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 ms41.3 MB代码结构清晰,性能适中
Python40 ms17.8 MB代码简洁,性能良好
C++8 ms21.2 MB执行速度最快,内存占用适中

代码亮点

  1. 🎯 递归解法简洁明了,直接反映了问题的本质
  2. 💡 BFS解法通过同时存储节点和路径和,避免了重复计算
  3. 🔍 正确处理了叶子节点的定义和空树的边界情况
  4. 🎨 两种解法各有优势,递归代码简洁,迭代避免栈溢出风险

常见错误分析

  1. 🚫 忽略了题目要求必须是从根节点到叶子节点的路径
  2. 🚫 没有正确处理空树的情况
  3. 🚫 错误地认为任何节点都可以作为路径的终点
  4. 🚫 在递归过程中没有正确更新目标和

解法对比

解法时间复杂度空间复杂度优点缺点
递归(DFS)O(n)O(h)代码简洁,直观易懂对于高度较大的树可能导致栈溢出
迭代(BFS)O(n)O(n)避免栈溢出,适用于所有树代码稍复杂,需要额外空间存储路径和

相关题目