【LeetCode刷题日记】递归与回溯实战 257.二叉树的所有路径——一篇文章彻底搞懂回溯

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!

前言:

大家好,我是代码不加冰,今天我们来学习二叉树相关的算法题,具体是关于二叉树的路径问题,其实也就是递归的问题,我们后面要学的回溯的实现通常就是使用递归来实现的,所以还是很重要的,让我们来具体看看吧。

摘要:

本文探讨了二叉树路径问题的递归解法。题目要求返回所有从根节点到叶子节点的路径,通过前序遍历和回溯算法实现。核心思路是:使用path列表记录当前路径,遇到叶子节点时将路径转为字符串存入result;递归访问左右子树后,通过path.remove()实现回溯。文章详细解析了递归函数各环节的实现逻辑,包括路径记录、叶子节点判断、递归调用和回溯操作,并提供了完整的Java代码实现。该方法有效解决了二叉树路径遍历问题,时间复杂度为O(n),空间复杂度为O(h),其中h为树的高度。

题目背景:

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

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

示例 1:

复制代码
输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]

示例 2:

复制代码
输入:root = [1]
输出:["1"]

提示:

  • 树中节点的数目在范围 [1, 100]
  • -100 <= Node.val <= 100

题目解析:

根据题目分析,并不是简单的遍历问题,我们需要返回所有路径,因此即便我们走过了之前的节点,也要回去,看那个节点还有没有别的路径,这叫回溯(听着挺高大上的),其实就是跟开车差不多,在一个路口有多条路,我们在路口这个节点可以选择一个,但是之后还要回到路口,把剩下的路一条条的走完。

核心要点
要点 说明
遍历方式 前序遍历(根 → 左 → 右)
何时记录 遇到叶子节点(左右都为空)
何时回溯 离开当前节点时,从路径中删除它
路径表示 List<String>StringBuilder 维护
具体的实现例子:

需要注意:

  • 前进:负责加节点、构建路径

  • 回溯:负责删节点、清理现场

  • 路径是维护在一个全局(或传参)的列表里的

  • 回溯删除的是 当前节点

  • 根节点不会被删除,除非整个递归全部结束

根节点一直保留,是因为还没从根节点回溯回去,我们回溯每次只回到上一个节点,但是题目要求返回的是从根节点到所有子节点的完整路径,对于一开始刚学回溯的同学来说可能容易混淆,错误的认为回溯是回到第一个节点,然后从头开始,如果真是这样,可能会导致后面的路径丢失,遍历不完整个树。

理解 认为回溯 后果
❌ 错误 回到根节点,从头开始 路径永远只有一条,永远走不完所有组合
✅ 正确 回到上一个节点,继续尝试 路径逐步构建、撤销,覆盖所有可能性

理解完这些,那么这题就很简单了,仅仅是代码实现的问题了。

在二叉树路径问题中,通常有两个容器:

容器 作用 存储内容 示例
result 存储最终结果 所有路径字符串 ["1->2->5", "1->3"]
path 存储当前正在走的路径 当前路径的节点值 [1,2,5]
  • path临时工 :不断变化,记录当前走到哪了,不断变化,回溯删除

  • result永久存储:一旦记录一条完整路径,就永久保存

第1段:主方法

java

复制代码
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;
}

解释

  • result:最终要返回的结果,装所有路径字符串

  • 如果树是空的,直接返回空列表(题目要求)

  • path:临时记录当前走的路径(比如 [1,2,5]

  • 调用 dfs 开始深度优先搜索

第2段:递归函数开头------做选择

java

复制代码
private void dfs(TreeNode node, List<Integer> path, List<String> result) {
    path.add(node.val);

解释

  • 每进入一个节点,就把它的值加入 path

  • 例如从节点1进入节点2:path[1] 变成 [1,2]

这就是"做选择"------把当前节点纳入当前路径

第3段:叶子节点判断

java

复制代码
if (node.left == null && node.right == null) {
    result.add(buildPath(path));
}

解释

  • 如果当前节点没有左孩子也没有右孩子 → 说明是叶子节点

  • 一条完整的路径已经形成(从根到当前叶子)

  • 调用 buildPath[1,2,5] 转成 "1->2->5"

  • 加入 result

示例 :走到节点5时,path = [1,2,5],记录下 "1->2->5"

第4段:非叶子节点------继续递归

java

复制代码
else {
    if (node.left != null) dfs(node.left, path, result);
    if (node.right != null) dfs(node.right, path, result);
}

解释

  • 如果不是叶子,说明还可以往下走

  • 有左孩子就递归左子树

  • 有右孩子就递归右子树

  • 注意:递归完成后会回到这里,继续往下执行

第5段:回溯------撤销选择

java

复制代码
path.remove(path.size() - 1);

解释

  • 这是最容易被忽略的一行

  • 当前节点的左右子树都探索完了,要离开这个节点

  • 离开之前,必须把它从 path 中删除

  • 这样返回上一层时,path 还是原来的样子

示例

  • 进入5之前:path = [1,2]

  • 进入5:path = [1,2,5]

  • 记录完路径,离开5:path = [1,2] ← 恢复原样

  • 这样节点2才能继续去探索其他孩子

第6段:格式化方法

java

复制代码
private String buildPath(List<Integer> path) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < path.size(); i++) {
        if (i > 0) sb.append("->");
        sb.append(path.get(i));
    }
    return sb.toString();
}

解释

  • [1,2,5] 转成 "1->2->5"

  • StringBuilder 效率更高

  • if (i > 0) 确保第一个数字前面不加 ->


题目答案:

java 复制代码
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
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;
    
    }
    private void dfs(TreeNode node,List<Integer>path,List<String> result){
        path.add(node.val);
            if (node.left == null && node.right == null) {
            result.add(buildPath(path));
        } else {
            // 3. 继续递归探索子节点
            if (node.left != null) dfs(node.left, path, result);
            if (node.right != null) dfs(node.right, path, result);
        }
          path.remove(path.size() - 1);

    }
    private String buildPath(List<Integer> path){
        StringBuilder sb=new StringBuilder();
        for(int i=0;i<path.size();i++){
            if(i>0) sb.append("->");
            sb.append(path.get(i));
        }
        return sb.toString();
    }
}

结语:如果对你有帮助,请**点赞,关注,收藏,**你的支持就是我最大的鼓励!

相关推荐
小欣加油几秒前
leetcode3635 最早完成陆地和水上游乐设施的时间II
数据结构·c++·算法·leetcode
qq_458148206 分钟前
科大讯飞实时语音识别(rtasr)真实项目踩坑经验总结与手把手教学真实可运行Demo
java·开发语言·websocket·语音识别
三品吉他手会点灯6 分钟前
C语言学习笔记 - 46.运算符和表达式 - 运算符4 - 对初学运算符的一些建议
c语言·开发语言·笔记·学习
创业之路&下一个五年9 分钟前
mvvm中v和vm关系,vm中v和m的关系?
java·开发语言·javascript
SilentSamsara10 分钟前
缓存策略实战:Redis + Python 多级缓存设计与失效策略
开发语言·redis·python·缓存·性能优化
zlinear数据采集卡14 分钟前
输出短路保护电路深度解析:从电源的“最后一道防线”到ZLinear采集卡的硬核守护实战
开发语言·嵌入式硬件·持续集成
本地化文档15 分钟前
psycopg3-docs-l10n
数据库·python·postgresql·github·gitcode·sphinx
剑锋所指,所向披靡!17 分钟前
C++多线程实现
开发语言·c++·chrome
GUO_PP24 分钟前
win11英雄联盟打开以后,自动改变音效,开启免提模式的问题修正
人工智能·算法
十五年专注C++开发24 分钟前
Qt之QScopedPointer、QScopeGuard、QScopedValueRollback使用及源码解读
开发语言·c++·qt·qscopedpointer·qscopeguard