二叉树的递归/层序遍历

简介: 递归遍历(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() 作为循环条件。

相关文章
|
23天前
|
存储 人工智能 监控
AI 智能体的开发流程
国内AI智能体开发已步入企业级全生命周期管理阶段。本文系统梳理2026主流实践:从业务拆解、模型选型、核心能力构建(规划/记忆/工具/角色)、工作流编排,到测试评估、安全部署与持续运营,覆盖国产化落地关键路径。(239字)
|
网络协议 Unix API
iOS - Socket 网络套接字
1、Socket 套接字 所谓 Socket,通常称为 “套接字”,网络应用程序通过套接字向网络发送请求或者应答网络请求。Socket 通常用于描述 IP 地址和端口,是应⽤层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口,是一个通信链的句柄,可以用来实现不同虚拟机或者不同计算机之间的通信。
2300 0
|
网络协议 Linux C++
Linux C/C++ 开发(学习笔记十一 ):TCP服务器(并发网络网络编程 一请求一线程)
Linux C/C++ 开发(学习笔记十一 ):TCP服务器(并发网络网络编程 一请求一线程)
459 0
|
存储 运维 监控
Linux--深入理与解linux文件系统与日志文件分析
深入理解 Linux 文件系统和日志文件分析,对于系统管理员和运维工程师来说至关重要。文件系统管理涉及到文件的组织、存储和检索,而日志文件则记录了系统和应用的运行状态,是排查故障和维护系统的重要依据。通过掌握文件系统和日志文件的管理和分析技能,可以有效提升系统的稳定性和安全性。
388 7
|
SQL 人工智能 关系型数据库
我在IDEA编辑器中使用通义灵码
体验了通义千问后,我开始使用智能编码助手通义灵码,它让代码开发更加高效便捷。通过具体的应用场景,如项目私有化改造,利用通义灵码生成PO类和SQL脚本,大幅提升了开发效率。正确的使用姿势包括提供提示词和多次尝试,能够更好地发挥通义灵码的智能辅助功能。
1178 2
|
关系型数据库 MySQL 数据库
上手体验 PolarDB-X 数据库
PolarDB-X,一款高性能云原生分布式数据库。
974 2
|
存储 消息中间件 缓存
Lustre架构介绍的阅读笔记-NFS兼容性
Lustre是分布式NFS系统,融合了分布式系统和NFS特性。它支持线性扩展容量和性能,提供POSIX语义,隐藏复杂存储细节。关键技术涉及分布式计算、缓存、锁、事务、通信(RPC、消息队列、同步/异步模式)、选举、任务调度、健康检查、负载均衡、集群管理和QoS。数据一致性、复制(副本、EC)、热点管理及多种上层协议(如NFS、S3)也是重点。分布式存储通过扩容提升读写带宽和IOPS。
581 1
|
算法
【数据结构与算法 刷题系列】判断链表是否有环(图文详解)
【数据结构与算法 刷题系列】判断链表是否有环(图文详解)
|
小程序 API
uni-app上传音频,图片步骤
uni-app上传音频,图片步骤
708 0
|
存储 算法 Unix
41. 【Linux教程】日志文件系统
41. 【Linux教程】日志文件系统
322 0