leetcode112, 257:二叉树的路径总和、二叉树的所有路径两题多解深度对比

文章目录

  • [LeetCode 112: 路径总和](#LeetCode 112: 路径总和)
    • [一、 题目描述](#一、 题目描述)
    • [二、 核心思路与解法分析](#二、 核心思路与解法分析)
    • 参考解法
  • [LeetCode 257: 二叉树的所有路径](#LeetCode 257: 二叉树的所有路径)
  • [三、 核心对比与总结](#三、 核心对比与总结)

二叉树路径问题LeetCode 112 (路径总和) 【难度:简单;通过率:55.5%】和 LeetCode 257 (二叉树的所有路径)【难度:简单;通过率:71.7%】

这两道题都围绕"从根到叶子的路径"展开,是理解 DFS、回溯思想以及递归参数设计的绝佳案例


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

解释: 存在路径 5 -> 4 -> 11 -> 2,其和为 22

示例 2:

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

输出: false

二、 核心思路与解法分析

目标: 寻找是否存在一条满足条件的路径

思路: 采用深度优先搜索(DFS)。我们从根节点出发,每往下走一步,就将当前路径和累加。当到达一个叶子节点时,检查当前路径和是否等于 targetSum

参考解法

【一种直观的参考代码】:

java 复制代码
public class Solution {
    int target;
    boolean isExist = false; // 找到了

    // 分而治之 + 递归 + 回溯
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return false;
        }
        target = targetSum;
        // 开启dfs
        dfs(0, root);
        return isExist;
    }

    public void dfs(int curSum, TreeNode curNode) {
        // 先校验基准情况 (结尾必须是叶子节点)
        if ((curSum + curNode.val == target || isExist) && isLeaf(curNode)) {
            isExist = true;
            return;
        }

        // 向左子树搜索
        if (curNode.left != null) {
            dfs(curSum + curNode.val, curNode.left);
        }

        // 向右子树搜索
        if (curNode.right != null) {
            dfs(curSum + curNode.val, curNode.right);
        }
    }

    public boolean isLeaf(TreeNode node) {
        return node.left == null && node.right == null;
    }
}

关键点解析:

  1. 隐式回溯sumint 类型,按值传递。dfs 调用返回后,当前栈帧的 sum 值不变,天然地完成了"回溯"
  2. 剪枝isExist 的判断非常关键。一旦找到一条路径,后续的所有递归分支都可以被跳过,大大提高了效率

LeetCode 257: 二叉树的所有路径

一、 题目描述

给你一个二叉树的根节点 root,按 任意顺序 返回所有从根节点到叶子节点的路径

示例:

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

输出: ["1->2->5", "1->3"]

二、 核心思路与解法分析

目标: 寻找并记录所有满足条件的路径

思路: 同样采用深度优先搜索(DFS)。但这次我们不仅要累加和,还要记录下路径本身。当到达叶子节点时,将记录好的路径格式化为字符串并存入结果列表

参考解法 (标准回溯解法)

由于需要记录路径,我们必须在递归时维护一个路径列表,并在回溯时手动将其恢复原状(最终再记录结果)

java 复制代码
class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> result = new ArrayList<>();
        if (root == null) {
            return result;
        }
        List<Integer> path = new ArrayList<>();
        dfs(root, path, result);
        return result;
    }

    /**
     * 深度优先搜索
     * @param node   当前节点
     * @param path   记录从根到当前节点的路径节点值
     * @param result 存储最终结果
     */
    public void dfs(TreeNode node, List<Integer> path, List<String> result) {
        // 1. 将当前节点加入路径
        path.add(node.val);

        // 2. 判断是否到达叶子节点
        if (node.left == null && node.right == null) {
            // 到达叶子节点,构建路径字符串并加入结果集
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < path.size() - 1; i++) {
                sb.append(path.get(i)).append("->");
            }
            sb.append(path.get(path.size() - 1));
            result.add(sb.toString());
            // 注意:这里不能直接 return,因为后面还有回溯操作
        }

        // 3. 递归探索子节点
        if (node.left != null) {
            dfs(node.left, path, result);
        }
        if (node.right != null) {
            dfs(node.right, path, result);
        }

        // 4. 显式回溯:
        // 离开当前节点前,将它从路径中移除,以便其他分支使用干净的 path
        path.remove(path.size() - 1);
    }
}

关键点解析:

  1. 显式回溯pathList 类型,按引用传递。所有递归分支共享同一个 path 对象。因此,当一个节点的子树探索完毕,准备返回父节点时,必须通过 path.remove(...) 手动将当前节点从路径中移除,这便是"显式回溯"
  2. 不可剪枝:因为题目要求找到"所有"路径,所以即使找到了第一条,也必须继续探索其他所有分支

拓展

注意,我们也可以这么做:合理设置 dfs 的参数 ,来优雅地避免手动回溯 路径列表吗,实现路径列表的自动回溯,且这种代码应该更好理解。参考代码如下(边搜索边记录结果):

java 复制代码
class Solution {
    List<String> result = new ArrayList<>();

    // dfs 分左右子树搜索,并且每个递归栈帧校验(是否到达叶子节点,是则收集,否则回溯)
    public List<String> binaryTreePaths(TreeNode root) {
        if (root == null) {
            return null;
        }
        // 从root开始dfs(path是不含)
        dfs("", root);
        return result;
    }

    /**
     * @param path 当前收集到的路径(暂不含curNdoe)
     * @param curNode 当前节点
     */
    public void dfs(String path, TreeNode curNode) {
        // 校验终止条件,就一个:到达叶子结点
        if (curNode.left == null && curNode.right == null) {
            result.add(path + curNode.val); // 拼接字符串
            return;
        }

        // 搜索左子树
        if (curNode.left != null) {
            dfs(path + curNode.val + "->", curNode.left);
        }

        // 搜索右子树
        if (curNode.right != null) {
            dfs(path + curNode.val + "->", curNode.right);
        }
    }
}
  • 当然,上述代码含有大量字符串的连接和创建,这是一笔不小的开销,虽然可以提交通过,但是时间不低
  • 后续的优化思路可以从这里下手,使用StringBuilder或者栈等数据结构优化,也可以达到 1ms 的提交,不过最终可能还是避免不了使用一些手动的回溯

最终兼顾效率与高可读性的参考代码(也是边搜索边记录结果):

java 复制代码
class Solution {
    List<String> result = new ArrayList<>();

    public List<String> binaryTreePaths(TreeNode root) {
        if (root == null) {
            return result;
        }
        dfs(new StringBuilder(), root);
        return result;
    }

    public void dfs(StringBuilder pathBuilder, TreeNode node) {
    	// 保存进入当前递归的路径情况(保存长度即可),用于后续回溯
        int originalLength = pathBuilder.length();

        // 先把当前节点值加上
        pathBuilder.append(node.val);

        // 判断是否到达叶子节点
        if (node.left == null && node.right == null) {
            result.add(pathBuilder.toString());
            pathBuilder.setLength(originalLength); // 返回之前也要手动回溯一下!
            return; // 叶子节点直接返回
        }
        // 非叶子节点,继续追加 "->"
        pathBuilder.append("->");

        // 搜索左子树
        if (node.left != null) {
            dfs(pathBuilder, node.left);
        }
        // 搜索右子树
        if (node.right != null) {
            dfs(pathBuilder, node.right);
        } 		// 回溯当前层节点
        pathBuilder.setLength(originalLength);
    } 
} 

三、 核心对比与总结

特性 LeetCode 112 (路径总和) LeetCode 257 (二叉树的所有路径)
问题目标 判断存在性 (返回 boolean) 记录所有解 (返回 List<String>)
遍历方式 深度优先搜索 (DFS) 深度优先搜索 (DFS)
回溯方式 隐式回溯 - 传递基本类型 int,按值传递,递归返回后自动恢复。 显式回溯 - 传递引用类型 List,必须手动 remove 恢复状态。
剪枝 可以剪枝 - 找到一个解后即可停止搜索。 不可剪枝 - 必须遍历整棵树找到所有解。
参数设计 dfs(curSum, node) dfs(path, node)
核心逻辑 在叶子节点判断 sum == target 在叶子节点构建并保存 路径字符串 或者 边搜索边保存

总结:

通过对比这两道题,我们可以深刻理解回溯思想的两种实现方式:

  • 当递归只需要传递数值(如 int, double)这类基本数据类型 时,通常是隐式回溯
  • 当需要维护一个集合(如 List, Set, StringBuilder)这类引用数据类型 来记录路径或状态时,就必须进行显式回溯,在递归返回前手动恢复状态