[数据结构·树] 二叉树的遍历方法及应用
二叉树的遍历是指按照某种特定的顺序访问树中的每一个节点,且每个节点只被访问一次。常见的遍历方法主要分为两大类:深度优先搜索(DFS)和广度优先搜索(BFS)。
- 深度优先搜索 (DFS):尽可能深地搜索树的分支。根据根节点被访问的顺序,又分为前序遍历、中序遍历和后序遍历。
- 广度优先搜索 (BFS):又称为层序遍历,从根节点开始,逐层地访问节点。
1. 前序遍历 (Pre-order Traversal)
访问顺序:根 (Root) -> 左 (Left) -> 右 (Right)
核心思想:首先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树。
递归实现思路:1
2
3
4
5
6
7
8
9
10
11
12
13
function preorder(node):
if node is null:
return
visit(node) // 访问根节点
preorder(node.left) // 遍历左子树
preorder(node.right) // 遍历右子树
迭代实现思路(使用栈):
- 将根节点入栈。
- 循环直到栈为空:
a. 弹出栈顶节点node并访问它。
b. 先将node的右子节点(如果存在)入栈。
c. 再将node的左子节点(如果存在)入栈。(注意:先入右再入左,保证出栈时先左后右)
适用情况与题型
树的复制/序列化:
- 分析:前序遍历的顺序(根-左-右)非常适合构建树。在序列化时,可以方便地标记空节点(例如用
#表示)。反序列化时,第一个值就是根,然后递归地构建左子树和右子树。 - 题型举例:LeetCode 297. 二叉树的序列化与反序列化
- 示例:树
[1, 2, 3, null, null, 4]序列化为"1,2,#,#,3,4,#,#,"。
- 分析:前序遍历的顺序(根-左-右)非常适合构建树。在序列化时,可以方便地标记空节点(例如用
确定树的结构(例如前缀表达式):
- 分析:表达式树的前序遍历结果即为前缀表达式(波兰表达式)。
- 题型举例:构建表达式树或根据树生成前缀表达式。
文件系统遍历:
- 分析:类似于操作系统的目录遍历,先访问当前目录(根),然后依次进入子目录(左、右...)。
2. 中序遍历 (In-order Traversal)
访问顺序:左 (Left) -> 根 (Root) -> 右 (Right)
核心思想:首先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。
递归实现思路:1
2
3
4
5
6
7
8
9
10
11
12
13
function inorder(node):
if node is null:
return
inorder(node.left) // 遍历左子树
visit(node) // 访问根节点
inorder(node.right) // 遍历右子树
迭代实现思路(使用栈):
- 初始化一个栈
stack和一个指针curr指向根节点。 - 循环条件:
curr不为空或stack不为空。 - 内部循环(向左走到底):当
curr不为空时,将curr入栈,并令curr = curr.left。 - (
curr为空,说明左子树到底了)从stack弹出一个节点node,访问node。 - 令
curr = node.right(转向右子树)。
适用情况与题型
二叉搜索树 (BST) 的排序输出:
- 分析:中序遍历二叉搜索树(BST)会得到一个非递减(升序)的节点值序列。这是 BST 最重要的特性之一。
- 题型举例:
- LeetCode 98. 验证二叉搜索树:中序遍历,检查序列是否严格递增。
- LeetCode 230. 二叉搜索树中第K小的元素:中序遍历,找到第 k 个被访问的元素。
- LeetCode 530. 二叉搜索树的最小绝对差:利用中序遍历得到有序序列,再找相邻元素的最小差值。
获取有序数据:
- 分析:当需要将树中的元素按某种“中间”逻辑(通常是大小)排序时,中序遍历是首选。
3. 后序遍历 (Post-order Traversal)
访问顺序:左 (Left) -> 右 (Right) -> 根 (Root)
核心思想:首先递归地后序遍历左子树,然后递归地后序遍历右子树,最后访问根节点。
递归实现思路:1
2
3
4
5
6
7
8
9
10
11
12
13
function postorder(node):
if node is null:
return
postorder(node.left) // 遍历左子树
postorder(node.right) // 遍历右子树
visit(node) // 访问根节点
迭代实现思路(使用两个栈或一个栈 + 标记):
一个较巧妙的方法是利用前序遍历(根-左-右)的变种(根-右-左),然后将结果反转,即得到(左-右-根)。
- 修改迭代前序遍历:入栈顺序改为先左后右。
- 访问顺序变为:根 -> 右 -> 左。
- 将访问结果存储起来,最后反转该结果。
适用情况与题型
树的销毁(释放内存):
- 分析:必须先处理(销毁)完左右子节点后,才能销毁根节点,否则会丢失对子节点的引用。后序遍历的顺序(左-右-根)完美符合这个要求。
- 题型举例:涉及释放树节点内存的系统编程。
依赖子树结果的计算(树型 DP):
- 分析:当父节点的计算依赖于其左右子树的计算结果时,必须使用后序遍历。你需要先得到左子树的结果和右子树的结果,才能计算当前根节点的结果。
- 题型举例:
- LeetCode 104. 二叉树的最大深度:
depth(node) = 1 + max(depth(node.left), depth(node.right))。 - LeetCode 110. 平衡二叉树:需要左右子树的高度来判断当前节点是否平衡。
- LeetCode 543. 二叉树的直径:通过一个节点的直径是其
max_depth(left) + max_depth(right)。 - LeetCode 124. 二叉树中的最大路径和:复杂树型 DP,依赖左右子树贡献的最大路径和。
- LeetCode 104. 二叉树的最大深度:
计算后缀表达式(逆波兰表达式):
- 分析:表达式树的后序遍历结果即为后缀表达式。
4. 层序遍历 (Level-order Traversal / BFS)
访问顺序:按层(从上到下)遍历,每层从左到右访问。
核心思想:使用队列(Queue)来实现广度优先搜索 (BFS)。
迭代实现思路(使用队列):
- 初始化一个队列
queue,将根节点入队。 - 循环直到队列为空:
a. 获取当前队列的大小levelSize(即当前层的节点数)。
b. 循环levelSize次:
c. (i. 将队首节点 `node` 出队并访问它。 ii. 将 `node` 的左子节点(如果存在)入队。 iii. 将 `node` 的右子节点(如果存在)入队。levelSize次循环结束,表示当前层已访问完毕)
适用情况与题型
按层打印树:
- 分析:这是层序遍历最直接的应用。
- 题型举例:LeetCode 102. 二叉树的层序遍历
寻找最短路径(在树中):
- 分析:BFS 的特性是能找到从起点到终点的最短路径(在图中)。在树中,它常用于查找最小深度。
- 题型举例:LeetCode 111. 二叉树的最小深度(注意:最小深度是从根到叶子节点的最短路径)。
获取特定层的信息:
- 分析:当题目要求涉及“层”、“宽度”、“最右/最左”的节点时,优先考虑层序遍历。
- 题型举例:
- LeetCode 199. 二叉树的右视图:层序遍历时,将每层最后一个访问的节点加入结果。
- LeetCode 637. 二叉树的层平均值
- LeetCode 103. 二叉树的锯齿形层序遍历
- LeetCode 515. 在每个树行中找最大值
总结
| 遍历方法 | 访问顺序 | 核心应用场景 | 典型题型 |
|---|---|---|---|
| 前序遍历 | 根 -> 左 -> 右 | 树的序列化、复制、构建 | LeetCode 297 (序列化) |
| 中序遍历 | 左 -> 根 -> 右 | 二叉搜索树 (BST) | LeetCode 98 (验证BST), 230 (BST第K小) |
| 后序遍历 | 左 -> 右 -> 根 | 依赖子树结果的计算 (树型DP)、释放内存 | LeetCode 104 (最大深度), 110 (平衡二叉树), 124 (最大路径和) |
| 层序遍历 (BFS) | 按层(上->下, 左->右) | 按层处理、最短路径(最小深度) | LeetCode 102 (层序遍历), 199 (右视图), 111 (最小深度) |
关键技巧:
- 前序 + 中序 或 后序 + 中序 可以唯一确定一棵二叉树(前提是树中没有重复元素)。
- 前序 + 后序 无法唯一确定一棵树。
- 解决二叉树问题时,首先思考 “对于当前节点
node,我需要从它的左、右子树获取什么信息?”- 如果需要先处理子树才能得到当前节点的结果 -> 后序遍历(树型 DP)。
- 如果处理顺序和子树无关,或者需要从上到下传递信息 -> 前序遍历。
- 如果和 BST 的大小顺序相关 -> 中序遍历。
- 如果和“层”、“深度”、“宽度”相关 -> 层序遍历 (BFS)。
