前置知识:二叉树的概念、性质与存储结构
二叉树的遍历
二叉树的遍历是指按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。
二叉树的递归特性:
①要么是棵空二叉树;
②要么就是由“根节点+左子树+右子树”组成的二叉树。
由此可知,遍历一棵二叉树只需确定对根结点
N
N
N、左子树
L
L
L和右子树
R
R
R的访问顺序即可。
typedef struct ElemType {
int value;
}ElemType;
typedef struct BiTNode {
ElemType data;
struct BiTNode* lchild, * rchild;
}BiTNode, * BiTree;
BiTree root;//声明一棵二叉树
1.先序遍历(
P
r
e
O
r
d
e
r
PreOrder
PreOrder)
若二叉树为空,则什么也不做;否则:
- 访问根结点;
- 先序遍历左子树;
- 先序遍历右子树。
对应的递归算法如下:
void PreOrder(BiTree T) {//先序遍历(根左右)
if (T != NULL) {
visit(T);//访问根结点
PreOrder(T->lchild);//递归访问左子树
PreOrder(T->rchild);//递归访问右子树
}
return;
}
2.中序遍历(
I
n
O
r
d
e
r
InOrder
InOrder)
若二叉树为空,则什么也不做;否则:
- 中序遍历左子树;
- 访问根结点;
- 中序遍历右子树。
对应的递归算法如下:
void InOrder(BiTree T) {//中序遍历(左根右)
if (T != NULL) {
InOrder(T->lchild);//递归访问左子树
visit(T);//访问根结点
InOrder(T->rchild);//递归访问右子树
}
return;
}
3.后序遍历(
P
o
s
t
O
r
d
e
r
PostOrder
PostOrder)
若二叉树为空,则什么也不做;否则:
- 后序遍历左子树;
- 后序遍历右子树。
- 访问根结点;
对应的递归算法如下:
void PostOrder(BiTree T) {//后序遍历(左右根)
if (T != NULL) {
InOrder(T->lchild);//递归访问左子树
InOrder(T->rchild);//递归访问右子树
visit(T);//访问根结点
}
return;
}
e . g . 求树的深度 e.g.\text{求树的深度} e.g.求树的深度
int TreeDepty(BiTree T) {//求树的深度
if (T == NULL)return 0;//若根结点为空返回0
else {
int l = TreeDepty(T->lchild);//递归求左子树深度
int r = TreeDepty(T->rchild);//递归求右子树深度
return l > r ? l + 1 : r + 1;//树的深度=max(左子树深度,右子树深度)+1
}
}
模板题:洛谷P4913 【深基16.例3】二叉树深度
AC代码放在我的Github:传送门
上述三种遍历算法中,递归遍历左、右子树的顺序都是固定的,区别只是访问根结点的顺序不同。不管采用哪种方式每个结点都访问一次且仅访问一次,所以时间复杂度都是 O ( n ) O(n) O(n)。在递归遍历中,递归工作栈的栈深恰好为树的深度。
模板题:【洛谷B3642】二叉树的遍历
题目描述
有一个
n
(
n
≤
1
0
6
)
n(n \le 10^6)
n(n≤106) 个结点的二叉树。给出每个结点的两个子结点编号(均不超过
n
n
n),建立一棵二叉树(根节点的编号为
1
1
1),如果是叶子结点,则输入 0 0
。
建好树这棵二叉树之后,依次求出它的前序、中序、后序列遍历。
输入格式
第一行一个整数 n n n,表示结点数。
之后 n n n 行,第 i i i 行两个整数 l l l、 r r r,分别表示结点 i i i 的左右子结点编号。若 l = 0 l=0 l=0 则表示无左子结点, r = 0 r=0 r=0 同理。
输出格式
输出三行,每行 n n n 个数字,用空格隔开。
第一行是这个二叉树的前序遍历。
第二行是这个二叉树的中序遍历。
第三行是这个二叉树的后序遍历。
样例 #1
样例输入 #1
7
2 7
4 0
0 0
0 3
0 0
0 5
6 0
样例输出 #1
1 2 4 3 7 6 5
4 3 2 1 6 5 7
3 4 2 5 6 7 1
AC代码如下:(递归实现)
#define _CRT_SECURE_NO_WARNINGS 1
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
using namespace std;
const int MAXN = 1e6 + 7;
struct TreeNode {
int lchild = 0, rchild = 0;
}t[MAXN];
int n;
inline void Visit(int p) {
cout << p << " ";
return;
}
inline void PreOrder(int p) {
if (!p)return;
Visit(p);
PreOrder(t[p].lchild);
PreOrder(t[p].rchild);
return;
}
inline void InOrder(int p) {
if (!p)return;
InOrder(t[p].lchild);
Visit(p);
InOrder(t[p].rchild);
return;
}
inline void PostOrder(int p) {
if (!p)return;
PostOrder(t[p].lchild);
PostOrder(t[p].rchild);
Visit(p);
return;
}
int main() {
cin >> n;
int l, r;
for (int i = 1; i <= n; ++i) {
cin >> l >> r;
t[i].lchild = l, t[i].rchild = r;
}
PreOrder(1);
cout << endl;
InOrder(1);
cout << endl;
PostOrder(1);
cout << endl;
return 0;
}
408考研初试中,对非递归算法的要求通常不高。但是该学的还是要学的。
※递归算法和非递归算法的转换
以中序遍历为例,由栈的思想分析其过程:
①沿着根的左孩子,依次入栈,直到左孩子为空,说明此时已找到可以输出的结点。
②栈顶元素出栈并访问:若其右孩子为空,继续执行②;否则对其右子树执行①。
(图片来自王道408数据结构考研复习指导2025)
图中二叉树的中序遍历序列为:
D
B
E
A
C
DBEAC
DBEAC
由分析可写出代码:
typedef struct LNode {//单链表存储栈
struct BiTNode* Node;//存放二叉树结点
struct LNode* next;
}LNode,*LinkList;
void InitLink(LinkList & S) {//初始化栈
S = (LinkList)malloc(sizeof(LNode));
if (S == NULL)return;
S->next = NULL;
return;
}
bool Is_Empty(LinkList S) {//判栈空
return S->next == NULL ? true : false;
}
void Push(LinkList& S, BiTree p) {//入栈
LNode* q = (LNode*)malloc(sizeof(LNode));
q->Node = p;
q->next = S->next;
S->next = q;
return;
}
void Pop(LinkList& S, BiTree& p) {//出栈
if (Is_Empty(S))return;
LNode* q = S->next;
p = q->Node;
S->next = q->next;
free(q);
return;
}
void InOrder2(BiTree T) {//中序遍历二叉树的非递归算法
LinkList S;//声明栈
InitLink(S);//初始化栈
BiTree p = T;//遍历指针p
while (p || !Is_Empty(S)) {//栈不空或p不空时循环
if (p) {//一路向左
// visit(p);//若为前序遍历,则在此访问先当前结点,并入栈
Push(S, p);//当前结点入栈
p = p->lchild;//左孩子不空则一直向左走
}
else {//出栈,并转向出栈结点的右孩子
Pop(S, p);//栈顶元素出栈
visit(p);//访问出栈结点
p = p->rchild;//走向右子树,p赋值为当前结点的右孩子
}//返回循环
}
return;
}
完整代码可看我的Github:传送门
后序遍历的非递归实现是最难的,因为在后序遍历中要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问,才能访问根结点。
后序遍历算法的思路分析:从根节点开始,将其入栈,然后沿其左子树一直向下搜索,直到搜索到没有左孩子的结点,但是此时不能出栈并访问,因为若其有右子树,则还需按相同的规则对其右子树进行处理。直至上述操作进行不下去为止,此时若栈顶元素想要出栈并被访问,要么右子树为空,要么右子树刚被访问完(此时左子树早已访问完),这样才能保证正确的访问顺序。
这一部分内容并不重要,可跳过。
二叉树的层次遍历
对二叉树进行按层次的遍历,需要借助一个辅助队列实现,具体思想如下:
①首先将二叉树的根结点入队。
②若队列非空,则队头结点出队,并访问该结点。若该结点有左孩子,则将其左孩子入队;若该结点有右孩子,则将其右孩子入队。
③重复步骤②,直至队列为空。
需要的前置知识:队列的链式存储结构
预处理如下:
typedef struct ElemType {
int value;
}ElemType;
typedef struct BiTNode {
ElemType data;
struct BiTNode* lchild, * rchild;
}BiTNode, * BiTree;
BiTree root;//声明一棵二叉树
void Visit(BiTree p) {
cout << p->data.value << " ";
return;
}
typedef struct LinkNode {//单链表
BiTree Node;//数据域存放二叉树结点
struct LinkNode* next;
}LinkNode, * LinkList;
typedef struct {//链队列
LinkNode* front, * rear;
}LinkQueue;
void InitQueue(LinkQueue& Q) {//队列初始化(默认带头结点)
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
Q.front->next = NULL;
return;
}
bool Is_Empty(LinkQueue Q) {//队列判空
return Q.front == Q.rear ? true : false;
}
void EnQueue(LinkQueue& Q, BiTree T) {//入队
LinkNode* p = (LinkNode*)malloc(sizeof(LinkNode));
if (p == NULL)return;
p->Node = T;
p->next = NULL;
Q.rear->next = p;
Q.rear = p;
return;
}
void DeQueue(LinkQueue& Q, BiTree& p) {//出队
if (Is_Empty(Q))return;
LinkNode* h = Q.front->next;
p = h->Node;
Q.front->next = h->next;
if (Q.rear == h)Q.rear = Q.front;
free(h);
return;
}
二叉树的层次遍历实现如下:
void LevelOrder(BiTree T) {//二叉树层次遍历
LinkQueue Q;
InitQueue(Q);//初始化辅助队列
BiTree p;
EnQueue(Q, T);//根结点入队
while (!Is_Empty(Q)) {//当队列不为空时
DeQueue(Q, p);//队头结点出队
Visit(p);//访问队头结点
if (p->lchild != NULL)EnQueue(Q, p->lchild);
//若左孩子不为空,则左孩子入队
if (p->rchild != NULL)EnQueue(Q, p->rchild);
//若右孩子不为空,则右孩子入队
}
return;
}
完整代码可以看我的GitHub:传送门
上述二叉树层次遍历的算法可作为模板学习,可通过练习加深对程序执行过程的理解,从而熟练掌握并能手写出来。
由遍历序列构造二叉树
(这一部分没有对代码实现要求,会手推即可。)
对一棵给定的二叉树,其先序序列、中序序列、后序序列和层序序列都是确定的。但是,只给出四种遍历序列中的任意一种,是无法确定一棵二叉树的。若已知中序序列,再给出其他三种遍历序列中的任意一种,就可以唯一地确定一棵二叉树。
1. 由中序序列和先序序列构造一棵二叉树
先序序列中,第一个结点一定是二叉树的根结点;而中序序列中,根结点必然将中序序列分割成两个子序列,前一个子序列是根的左子树的中序序列,后一个子序列是根的右子树的中序序列。
根的左子树的先序序列和中序序列长度是一样的,可以在先序序列的第一位中找到根的左子树的根结点,再将根的左子树的中序序列分割成两个子序列处理。同理,根的右子树也可如此操作。
将整个二叉树递归分解下去,最终就能唯一地确定这棵二叉树。
分解到最后剩下两个或三个结点时,即可通过中序(左根右)和先序(根左右)的规则进行验证。
2. 由中序序列和后序序列构造一棵二叉树
后序序列的最后一个结点一定是二叉树的根结点,按照先序序列同样的思想,根结点可以将中序序列分割成两个子序列。递归分解下去,最后也能唯一地确定这棵二叉树。
分解到最后剩下两个或三个结点时,即可通过中序(左根右)和后序(左右根)的规则进行验证。
3.由中序序列和层序序列构造一棵二叉树
层序序列中,第一个结点一定是二叉树的根结点,这样又可以把中序序列分割成左子树的中序序列和右子树的中序序列。若存在左子树,层序序列的第二个结点一定是左子树的根,之后可对左子树再进一步划分;若存在右子树,层序序列中紧接着的下一个结点就一定是右子树的根,之后又可对右子树再进一步划分。
分解到最后剩下两个或三个结点时,即可通过中序(左根右)和层序遍历的规则进行验证。
需要注意的是,先序序列、后序序列和层序序列任意两辆组合,都无法唯一确定一棵二叉树。
例如:
B
←
A
B←A
B←A(
B
B
B是
A
A
A的左孩子),
A
→
B
A→B
A→B(
B
B
B是
A
A
A的右孩子)
这样的两棵二叉树,其先序序列均为
A
B
AB
AB,后序序列均为
B
A
BA
BA,层序序列均为
A
B
AB
AB。
遍历是二叉树各种操作的基础,例如求结点的双亲,求结点的孩子,求二叉树的深度,求叶结点的个数,判断两棵二叉树是否相等,等等问题,都是在遍历的过程中进行的。因此必须掌握二叉树的各种遍历过程,并能灵活运用以解决各种问题。
以上。