二叉树的递归/层序遍历

简介: 递归遍历(DFS)按固定顺序访问节点,前/中/后序取决于代码位置。层序遍历(BFS)借助队列实现,可逐层访问,常见写法能记录层数,适用于求深度、分层处理等场景。

递归遍历(DFS)
二叉树的遍历框架:
// 基本的二叉树节点
class TreeNode {
int val;
TreeNode left, right;
}

// 二叉树的遍历框架
void traverse(TreeNode root) {
if (root == null) {
return;
}
traverse(root.left);
traverse(root.right);
}
这段短小精干的代码为什么能遍历二叉树?又是以什么顺序遍历二叉树的?请看VCR

traverse 函数的遍历顺序就是一直往左子节点走,直到遇到空指针不能再走了,才尝试往右子节点走一步;然后再一直尝试往左子节点走,如此循环;如果左右子树都走完了,则返回上一层父节点。其实看代码也能看出来,先递归调用的 root.left,然后才递归调用的 root.right 嘛。
这个递归遍历节点顺序是固定的,务必记住这个顺序,否则你肯定玩不转二叉树结构。
那么有一些数据结构基础的读者肯定要问了:不对呀,只要上过大学的数据结构课程都知道,二叉树有前/中/后序三种遍历,会得到三种不同顺序的结果。为啥你这里说递归遍历节点的顺序是固定的呢?
这个问题很好,我来解答一下。
Important
递归遍历的顺序,即 traverse 函数访问节点的顺序确实是固定的。正如上面那个视频,root 指针在树上移动的顺序是固定的。
但是你在 traverse 函数中不同位置写代码,效果是可以不一样的。前中后序遍历的结果不同,原因是因为你把代码写在了不同位置,所以产生了不同的效果。
比方说,刚进入一个节点的时候,你还对它的子节点一无所知,而当你要离开一个节点的时候,它的所有子节点你都遍历过了。那么在这两种情况下写的代码,肯定是可以有不同的效果的。
你所谓的前中后序遍历,其实就是在上述二叉树遍历框架的不同位置写代码:
// 二叉树的遍历框架
void traverse(TreeNode root) {
if (root == null) {
return;
}
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
}
前序位置的代码会在进入节点时执行;中序位置的代码会在左子树遍历完成后,遍历右子树之前执行;后序位置的代码会在左右子树遍历完成后执行:

层序遍历(BFS)
上面的递归遍历是依赖函数堆栈递归遍历二叉树的,如果把递归函数 traverse 看做一个指针,那么这个指针在二叉树上游走的顺序大概是从最左侧开始,一列一列走到最右侧。
二叉树的层序遍历,顾名思义,就是一层一层地遍历二叉树。这个遍历方式需要借助队列来实现,而且根据不同的需求,主要有三种不同的写法,下面一一列举。
写法一
这是最简单的写法,代码如下:
void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
// 访问 cur 节点
System.out.println(cur.val);

    // 把 cur 的左右子节点加入队列
    if (cur.left != null) {
        q.offer(cur.left);
    }
    if (cur.right != null) {
        q.offer(cur.right);
    }
}

}
cur 变量在树上游走的顺序可以得到层序遍历是一层一层,从左到右的遍历二叉树节点
这种写法的优缺点
这种写法最大的优势就是简单。每次把队头元素拿出来,然后把它的左右子节点加入队列,就完事了。但是这种写法的缺点是,无法知道当前节点在第几层。知道节点的层数是个常见的需求,比方说让你收集每一层的节点,或者计算二叉树的最小深度等等。
所以这种写法虽然简单,但用的不多,下面介绍的写法会更常见一些。
写法二
对上面的解法稍加改造,就得出了下面这种写法:
void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue q = new LinkedList<>();
q.offer(root);
// 记录当前遍历到的层数(根节点视为第 1 层)
int depth = 1;

while (!q.isEmpty()) {
    int sz = q.size();
    for (int i = 0; i < sz; i++) {
        TreeNode cur = q.poll();
        // 访问 cur 节点,同时知道它所在的层数
        System.out.println("depth = " + depth + ", val = " + cur.val);

        // 把 cur 的左右子节点加入队列
        if (cur.left != null) {
            q.offer(cur.left);
        }
        if (cur.right != null) {
            q.offer(cur.right);
        }
    }
    depth++;
}

}
注意代码中的内层 for 循环:
int sz = q.size();
for (int i = 0; i < sz; i++) {
...
}
这个变量 i 记录的是节点 cur 是当前层的第几个,大部分算法题中都不会用到这个变量,所以你完全可以改用下面的写法:
int sz = q.size();
while (sz-- > 0) {
...
}
这个属于细节问题,按照自己的喜好来就行。
但是注意队列的长度 sz 一定要在循环开始前保存下来,因为在循环过程中队列的长度是会变化的,不能直接用 q.size() 作为循环条件。

相关文章
|
4天前
|
JavaScript 前端开发 算法
React框架
React是一个用于构建用户界面的JavaScript库,专注于视图层,采用虚拟DOM和Diff算法实现高效渲染。支持组件化开发、服务端渲染、状态管理,易于测试与集成,通过生命周期、setState机制及高阶组件等特性提升开发效率与性能。
|
4天前
|
存储 缓存 运维
一场FullGC故障排查
本文记录了一次JDOS容器CPU告警的排查过程,通过分析发现实际为JVM Full GC引发CPU占用升高。结合泰山与SGM监控,定位到堆内存中大对象导致老年代频繁占满。经JPofiler分析,确认问题源于将Excel数据以List&lt;Map&lt;String, String&gt;&gt;形式加载至内存,造成严重内存膨胀。最终提出优化方案:避免大对象驻留JVM或改用高效存储结构,降低GC压力。
|
3天前
|
人工智能 自然语言处理 Cloud Native
AI时代代码开发(DeepSeek+Cursor+Devbox)
AI时代重塑软件开发,本课程聚焦DeepSeek+Cursor+Devbox+Sealos工具链,实现自然语言到代码的零基础全栈开发。覆盖需求分析、数据库设计、编码测试至云部署全流程,助力开发者高效构建并上线项目,抢占智能开发先机。(238字)
|
3天前
|
NoSQL Linux Shell
MongoDB单机部署
本文介绍MongoDB在Windows与Linux系统的安装启动方法,包括下载、解压、配置数据目录及命令行或配置文件方式启动服务。支持设置环境变量、自定义端口与日志路径,并提供shell连接、图形化工具Compass使用指南,以及Linux下防火墙配置与服务关闭操作,确保单机部署稳定运行。
|
3天前
|
存储 JSON NoSQL
MongoDB常用命令
本文介绍MongoDB数据库操作,以文章评论系统为例,涵盖数据库与集合的创建、删除,文档的增删改查、批量操作、投影查询、分页排序及统计功能,详细说明数据插入、更新条件控制、分页查询等常用操作方法。
|
3天前
|
缓存 Java 数据库连接
mybatis常见配置
本文介绍MyBatis核心配置机制,涵盖属性加载优先级(方法参数 &gt; resource/url &gt; properties内嵌)、常用配置项如缓存、延迟加载、执行器类型等,并详解多环境配置方式及事务管理(JDBC与MANAGED)。适用于需灵活管理数据源与事务的开发者。
|
3天前
|
数据采集 领域建模 数据库
领域模型图(数据架构/ER图)
数据架构核心输出为ER图,包含实体、关系与属性。通过四色原型法进行领域建模:红色MI表时序事件,绿色PPT为参与方/物品/地点,黄色Role示角色,蓝色DESC提供描述信息。以风控系统为例,从业务流程中提取MI作为骨干,逐步补充PPT、Role和DESC,最终提炼出ER图,明确实体间一对一、一对多或多对多关系,构建清晰的数据模型。(239字)
|
3天前
|
存储 SQL 人工智能
AI时代代码开发(数据库设计)
本文介绍基于三范式与DDD的数据库设计流程,结合AI工具辅助分析页面原型,通过部门、员工及工作经历模块,演示表结构设计与优化过程,强调人工校验与调整的重要性,最终完成符合业务需求的数据库建模与测试数据构建。
|
3天前
|
存储 安全 开发工具
Git 的基础知识
在软件开发中,版本控制如Git至关重要,它支持代码历史追踪、团队协作、分支实验、错误回滚与代码审查。通过提供变更审计轨迹、备份恢复及功能隔离,提升开发效率与代码质量,助力团队高效协同,保障项目稳定演进。
|
3天前
|
JSON 人工智能 架构师
AI时代代码开发(接口设计)
本章节基于页面原型与接口模板,采用Restful风格设计部门与员工管理模块的API接口,涵盖查询、新增、修改、删除等功能,严格遵循JSON格式与字段规范,确保前后端高效对接。