Skip to content

路径总和 II

约 1643 字大约 5 分钟

DFSBFS归并

2025-03-01

113. 路径总和 II

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

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

示例 1:

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]

示例 2:

输入:root = [1,2,3], targetSum = 5
输出:[]

示例 3:

输入:root = [1,2], targetSum = 0
输出:[]

提示:

  • 树中节点总数在范围 [0, 5000]
  • -1000 <= Node.val <= 1000
  • -1000 <= targetSum <= 1000

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

无条件进入子树,检查叶子节点,全局变量。

在前序位置添加当前节点到路径中后,要更新路径和并检查更新后的状态是否是叶子节点且路径和是否为0

回溯包括DFS。DFS是先取一个满足条件的点,然后进入递归函数,回溯是无差别,直接从头开始递归。

class Solution {
    List<List<Integer>> ret = new LinkedList<List<Integer>>();
    Deque<Integer> path = new LinkedList<Integer>();

    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        dfs(root, targetSum);
        return ret;
    }

    public void dfs(TreeNode root, int targetSum) {
        // 特殊条件
        if (root == null) {
            return;
        }
        
        // offerLast返回boolean值,addLast出错的话会抛出异常
        path.offerLast(root.val);
        // 更新状态
        targetSum -= root.val;
        // 检查更新后的状态是否成熟。这段if代码放在前中后都可以,因为它是结束条件,与遍历顺序无关。
        if (root.left == null && root.right == null && targetSum == 0) {
            ret.add(new LinkedList<Integer>(path));
            // 下面两行代码可加可不加。但必须同时加或不加。
            // 返回到上一层递归前需要去掉root。
            path.removeLast();
            return;
        }
        // 我们要找到所有答案,且节点值可能为负,所以就是本步状态成熟也要继续遍历子树。意思就是不是叶子节点但是targetSum为0,也要继续递归。
        dfs(root.left, targetSum);
        dfs(root.right, targetSum);
        path.pollLast();
        // targetSum是局部变量,因此每次递归都有自己的独立的targetSum,不需要再执行targetSum += root.val。path是全局的,所以它要执行pollLast来复原。
    }
}
  • 时间复杂度:O(N^2),其中 N 是树的节点数。在最坏情况下,树的上半部分为链状,下半部分为完全二叉树,并且从根节点到每一个叶子节点的路径都符合题目要求。此时,路径的数目为 O(N),并且每一条路径的节点个数也为 O(N),因此要将这些路径全部添加进答案中,时间复杂度为 O(N^2)。

    解释一下时间复杂度:上半部分是N/2个节点,下半部分是N/2个,完全二叉树的高度是log2N/2\log_2 N/2,所以叶子节点的个数是 2log2N/2=N/22^{\log_2 N/2}=N/2 个,所以时间复杂度总共是 O(N^2)。

  • 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于栈空间的开销,栈中的元素个数不会超过树的节点数。

BFS

一个队列存节点,一个队列存路径和,一个临时变量存路径和,一个哈希表存父节点以获取路径。

因为我们需要获取所有的满足条件的路径,所以需要一个队列来存路径和。

class Solution {
    // 用于存储所有符合条件的路径
    List<List<Integer>> ret = new LinkedList<List<Integer>>();
    // 哈希表用于记录每个节点的父节点,以便于回溯路径
    Map<TreeNode, TreeNode> map = new HashMap<TreeNode, TreeNode>();

    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        // 如果根节点为空,直接返回空列表
        if (root == null) {
            return ret;
        }

        // 使用两个队列,分别存储节点和从根到当前节点的路径和
        Queue<TreeNode> queueNode = new LinkedList<TreeNode>();
        Queue<Integer> queueSum = new LinkedList<Integer>();
        // 初始化队列
        queueNode.offer(root);
        queueSum.offer(root.val);

        // 广度优先搜索
        while (!queueNode.isEmpty()) {
            TreeNode node = queueNode.poll();
            int rec = queueSum.poll(); // 更新当前路径和

            // 如果当前节点是叶子节点
            if (node.left == null && node.right == null) {
                // 如果当前路径和等于目标值,回溯并记录这条路径
                if (rec == targetSum) {
                    getPath(node);
                }
            } else {
                // 如果不是叶子节点,继续探索其子节点
                if (node.left != null) {
                    map.put(node.left, node); // 记录左子节点的父节点
                    
                    queueNode.offer(node.left); // 左子节点入队
                    queueSum.offer(rec + node.left.val); // 更新路径和入队
                }
                if (node.right != null) {
                    map.put(node.right, node); // 记录右子节点的父节点
                    
                    queueNode.offer(node.right); // 右子节点入队
                    queueSum.offer(rec.right.val); // 更新路径和入队
                }
            }
        }

        return ret;
    }

    // 从叶子节点回溯到根节点,记录路径
    public void getPath(TreeNode node) {
        List<Integer> temp = new LinkedList<Integer>();
        // 从当前节点开始,通过哈希表回溯到根节点
        while (node != null) {
            temp.add(0, node.val); // 将节点值添加到列表的开头,避免后续的翻转操作
            node = map.get(node); // 移动到父节点
        }
        // 一定要新建列表,不能直接add(temp),因为temp是引用类型,会被修改。
        ret.add(new LinkedList<Integer>(temp));
    }
}
  • 时间复杂度:O(N^2),其中 N 是树的节点数。分析思路与方法一相同。
  • 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于哈希表和队列空间的开销,哈希表需要存储除根节点外的每个节点的父节点,队列中的元素个数不会超过树的节点数。

有返回值有信息参数的DFS,后序归并。

class Solution {
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        List<List<Integer>> rootAnswers = new LinkedList<>();
        // 特殊条件
        if (root == null) {
            return rootAnswers;
        }
        // 结束条件:如果是叶子节点并且值等于 targetSum,则找到一条路径。这段代码在后序也可以。
        if (root.left == null && root.right == null && root.val == targetSum) {
            // 成功时才新建一个path
            LinkedList<Integer> path = new LinkedList<>();
            // path不是全局变量,所以不需要回溯,即pollLast()。
            path.add(root.val);
            
            rootAnswers.add(path);
            return rootAnswers;
        }

        // 分别递归左右子树,找到子树中以子节点为起点以叶子节点为终点的和为 targetSum - root.val 的路径
        List<List<Integer>> leftAnswers = pathSum(root.left, targetSum - root.val);
        List<List<Integer>> rightAnswers = pathSum(root.right, targetSum - root.val);

        // 左右子树的路径加上根节点,就是和为 targetSum 的路径
        for (List<Integer> answer : leftAnswers) {
            // 在链表头部添加root.val
            // 因为底层使用的是 LinkedList,所以这个操作的复杂度是 O(1)
            answer.add(0, root.val);
            rootAnswers.add(answer);
        }
        for (List<Integer> answer : rightAnswers) {
            // 在链表头部添加root.val
            // 因为底层使用的是 LinkedList,所以这个操作的复杂度是 O(1)
            answer.add(0, root.val);
            rootAnswers.add(answer);
        }

        return rootAnswers;
    }
}