第七章 二叉树
二叉树
二叉树的定义
二叉树是一种特殊的树型结构,它的特点是每个结点最多只有两颗子树(即二叉树中不存在度大于2的结点),且二叉树的子树有左右之分,其次序不能任意颠倒。
⼆叉的意思是这种树的每⼀个结点最多只有两个孩⼦结点。注意这⾥是多有两个孩⼦,也可能没有孩⼦或者是只有⼀个孩⼦。
⼆叉树结点的两个孩⼦,⼀个被称为左孩⼦,⼀个被称为右孩⼦。其顺序是固定的,就像⼈的左⼿和右⼿,不能颠倒混淆。
特殊的二叉树
满二叉树
一棵二叉树所有非叶子结点都存在左右孩子,并且所有叶子结点都在同一层级上,那么这棵树就是满二叉树。
完全二叉树
在满二叉树的基础上,从最底层最右边开始,连续去掉若干个结点,那么就形成了完全二叉树。
除了上述两种二叉树,还存在堆、二叉排序树等特殊的二叉树。
二叉树的存储
根据定义,二叉树是特殊类型的树,因此可以使用树的存储方式来存储二叉树。在此基础上,标记左右子树即可。
- 比如使用vector数组存储时,先尾插左孩子再尾插右孩子。这样输出时,就可以按照左孩子、右孩子的顺序输出。
- 比如使用链式前向星存储时,先头插左孩子再头插右孩子。注意,此时将会按照右孩子、左孩子的顺序输出。
但是,由于二叉树结构的特殊性,我们可以使用更符合其特性的存储方式:顺序存储和链式存储。
顺序存储
顺序存储是指使用数组来存储二叉树。对于完全二叉树,我们可以使用数组来存储。并且此时可以利用下标来计算左右孩子和父亲:
如果父存在,其下标为i/2;
如果左孩子存在,其下标为2i;
如果右孩子存在,其下标为2i+1;
当然,非完全二叉树也可以使用顺序存储,但是此时需要补全空结点,使其变为完全二叉树。
再来考虑其最差情况:如果是右斜树即每一个结点只有右孩子,存在4个结点,那么此时需要使用大小为24 - 1的数组来存储。会造成存储空间的极大浪费。因此,顺序存储一般只适用于完全二叉树或者满二叉树。
链式存储
在竞赛中,我们这里的链式存储依然是静态实现。
同时,在竞赛中,给定的树结构通常是有编号的,因此我们可以创建两个数组:l[]
用于存储左孩子,r[]
用于存储右孩子。其中,l[i]
表示结点i的左孩子,r[i]
表示结点i的右孩子。
注意,这里的编号是指结点的编号,而不是结点的值。
链式存储的优点是可以存储任意形状的二叉树,并且不需要补全空结点。
题目描述
有一个n( n <=106 )个结点的二叉树。给出每个结点的两个孩子结点的编号,建立一颗二叉树(根结点编号为1),求这棵树的深度。
输入描述
第一行一个整数n,表示n个结点。
接下来n行,第i行两个整数l和r,表示第i个结点的左孩子和右孩子的编号。
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int l[N],r[N];
int main(){
int n;
cin >> n;
for(int i = 1;i <= n;i++){
cin >> l[i] >> r[i];
}
return 0;
}
二叉树的遍历
基于上一章,二叉树同树一样,存在深度优先遍历(DFS)和宽度优先遍历(BFS)两种遍历方式。
深度优先遍历(DFS)
不同于树的深度优先遍历,二叉树的深度优先遍历有三种遍历方式:
- 前序遍历:先序遍历根结点,再先序遍历左子树,最后先序遍历右子树。
- 中序遍历:中序遍历左子树,再中序遍历根结点,最后中序遍历右子树。
- 后序遍历:后序遍历左子树,再后序遍历右子树,最后后序遍历根结点。
简单来说,前序遍历就是按照根、左、右的顺序遍历;中序遍历就是按照左、根、右的顺序遍历;后序遍历就是按照左、右、根的顺序遍历。
再简言之,三种遍历都是从根到子、先左再右的查找顺序之大前提下,前序遍历就是访问到结点就输出;中序遍历先找到并输出左孩子及其子树(子树中依旧先找左孩子及其子树),后输出根结点,最后输出右子树;后序遍历先找到并输出左子树,后找到并输出右子树,最后找全左右孩子及其子树后输出根结点。
另,以上查找孩子及其子树都基于其存在的前提下,注意另外判断,若存在,进行递归查找即可。
题目描述
有一个n( n <=10^6 )个结点的二叉树。给出每个结点的两个孩子结点的编号,建立一颗二叉树(根结点编号为1),求这棵树的深度。
输入描述
第一行一个整数n,表示n个结点。
接下来n行,第i行两个整数l和r,表示第i个结点的左孩子和右孩子的编号。
测试用例
测试⼀:
4
0 2
3 4
0 0
0 0
测试⼆:
2
2 0
0 0
测试三:
3
2 3
0 0
0 0
测试四:
7
2 3
0 4
5 6
0 0
0 0
7 0
0 0
代码实现
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int l[N],r[N];
/*先序遍历*/
void dfsx(int u){
cout << u << " ";
if(l[u]) dfsx(l[u]);
if(r[u]) dfsx(r[u]);
}
/*中序遍历*/
void dfsz(int u){
if(l[u]) dfsz(l[u]);
cout << u << " ";
if(r[u]) dfsz(r[u]);
}
/*后序遍历*/
void dfsh(int u){
if(l[u]) dfsh(l[u]);
if(r[u]) dfsh(r[u]);
cout << u << " ";
}
int main(){
int n;
cin >> n;
for(int i = 1;i <= n;i++){
cin >> l[i] >> r[i];
}
dfsx(1);
cout << endl;
dfsz(1);
cout << endl;
dfsh(1);
cout << endl;
return 0;
}
我们注意到,二叉树中不同于一般的树,没有存储父结点的信息,因此在遍历过程中,不需要另外数组进行标记是否访问过。
宽度优先遍历(BFS)
宽度优先遍历与常规树的宽度优先遍历相同,直接利用队列即可。
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
int l[N],r[N];
void bfs(int u){
queue<int> q;
q.push(u);
while(q.size()){
int t = q.front();
q.pop();
cout << t << " ";
if(l[t]) q.push(l[t]);
if(r[t]) q.push(r[t]);
}
}
int main(){
int n;
cin >> n;
for(int i = 1;i <= n;i++){
cin >> l[i] >> r[i];
}
bfs(1);
cout << endl;
return 0;
}