Skip to content

路径总和

约 1669 字大约 6 分钟

2025-02-28

112. 路径总和

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

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

示例 1:

img.png

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

示例 2:

img.png

输入: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

二叉树的遍历代码是动态规划和回溯算法的祖宗。

二叉树路径和问题:在本题中,我们只是单次遍历二叉树,访问每个节点一次,所以时间复杂度是 O(n)。

子集生成问题:在子集生成问题中,回溯算法需要生成所有可能的子集,对于 n 个元素,存在 2n2^n 个子集, 因此时间复杂度是 O(2n)O(2^n)。可以这么想,子集生成不是二叉树,而是根据输入的数组生成一颗多叉树。这颗多叉树的节点数就是2n2^n

有返回值有信息参数的DFS

分类:叶子节点和非叶子节点。局部变量,函数参数,返回值。

操作:检查左右子树中是否有和为sum-root.val的路径,走到null或叶子节点时确定返回什么。只要有一个有,就返回true。

注意,null未必一定在叶子节点下面,即处理null和叶子节点的情况必须同时保留。例子:左子节点不为空但右子节点为空的节点。

class Solution {
    // 从root出发是否有路径和为sum的路径。
    public boolean hasPathSum(TreeNode root, int sum) {
        // 特殊情况,输入数组为空或当前节点只有一个子节点
        if (root == null) {
            return false;
        }
        // 结束条件:走到叶子节点。重点。
        if (root.left == null && root.right == null) {
            return sum == root.val;
        }
        
        // 没有走到叶子节点,可以继续往下走。选择范围:left和right
        return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
    }
}
  • 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
  • 空间复杂度:O(H),其中 H 是树的高度。空间复杂度主要取决于递归时栈空间的开销, 最坏情况下,树呈现链状,空间复杂度为 O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(logN)O(\log N)

回溯,无返回值无信息参数的DFS

全局变量存路径和,在前序和后序位置变更。结束条件是遇到叶子节点。遇到空直接返回,遇到叶子节点需要判断返回什么。

class Solution {
    int target;
    boolean found = false;
    // 记录遍历过程中的路径和
    int curSum = 0;

    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return false;
        }
        this.target = targetSum;
        
        // dfs 没有target参数,所以要把targetSum赋给全局变量target,这样回溯方法中就能访问target了。
        dfs(root);
        return found;
    }

    // 二叉树遍历函数
    void dfs(TreeNode root) {
        // 特殊情况
        if (root == null) {
            return;
        }

        // 前序遍历位置
        curSum += root.val;
        // 结束条件:叶子节点。这段代码放在前序、中序、后序都可以。
        if (root.left == null && root.right == null) {
            if (curSum == target) {
                found = true;
            }
        }
        // 下面两行代码的顺序也无所谓。
        dfs(root.left);
        dfs(root.right);
        // 后序遍历位置
        curSum -= root.val;
    }
}

可以把前序遍历的代码:

        // 前序遍历位置
        curSum += root.val;
        if (root.left == null && root.right == null) {
            if (curSum == target) {
                found = true;
            }
        }

改成

        if (root.left == null && root.right == null) {
            if (curSum + root.val == target) { // 这里一定要加 root.val,不然检查的是上一轮回溯的值。
                found = true;
            }
        }

        // 前序遍历位置
        curSum += root.val;
  • 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
  • 空间复杂度:O(H),其中 H 是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(log⁡N)。

BFS层序遍历

两个队列,一个队列里存节点,一个队列里存路径和。每个节点都对应一个从根节点到该节点的路径和。节点入队时相应的路径和也要入队,出队时路径和也要出队。

走到叶子节点结束,判断返回什么。不需要判空,因为空节点不会入队。

class Solution {
    public boolean hasPathSum(TreeNode root, int sum) {
        if (root == null) {
            return false; // 如果根节点为空,则直接返回false
        }

        // 队列用于存储节点
        Queue<TreeNode> queNode = new LinkedList<>();
        // 队列用于存储从根节点到当前节点的路径和
        Queue<Integer> queVal = new LinkedList<>();
        
        queNode.offer(root); // 将根节点添加到节点队列中
        queVal.offer(root.val); // 将根节点的值添加到路径和队列中

        // 当节点队列不为空时,进行广度优先搜索
        while (!queNode.isEmpty()) {
            TreeNode currentNode = queNode.poll(); // 取出当前节点
            int currentSum = queVal.poll(); // 取出当前节点对应的路径和

            // 检查是否为叶子节点且路径和等于目标和
            if (currentNode.left == null && currentNode.right == null) {
                if (currentSum == sum) {
                    return true; // 如果是叶子节点且路径和等于目标和,返回true
                }
                continue; // 如果不是目标和,继续搜索其他路径。
            }

            // 如果左子节点不为空,将其加入队列并更新路径和
            if (currentNode.left != null) {
                queNode.offer(currentNode.left);
                queVal.offer(currentNode.left.val + currentSum);
            }
            // 如果右子节点不为空,将其加入队列并更新路径和
            if (currentNode.right != null) {
                queNode.offer(currentNode.right);
                queVal.offer(currentNode.right.val + currentSum);
            }
        }

        return false; // 如果搜索完所有路径后没有找到符合条件的路径,返回false
    }
}
  • 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
  • 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。队列里最多存储最下面一层的节点。