⼆叉树的概念
⼆叉树的定义
⼆叉树是⼀种特殊的树型结构,它的特点是每个结点⾄多只有2棵⼦树(即⼆叉树中不存在度⼤于2的结点),并且⼆叉树的⼦树有左右之分,其次序不能任意颠倒。
⼆叉的意思是这种树的每⼀个结点最多只有两个孩⼦结点。注意这⾥是最多有两个孩⼦,也可能没有孩⼦或者是只有⼀个孩⼦。
注意:⼆叉树结点的两个孩⼦,⼀个被称为左孩⼦,⼀个被称为右孩⼦。其顺序是固定的,就像⼈的左⼿和右⼿,不能颠倒混淆
特殊的⼆叉树
满⼆叉树
⼀棵⼆叉树的所有⾮叶⼦节点都存在左右孩⼦并且所有叶⼦节点都在同⼀层上,那么这棵树就称为满⼆叉树
完全⼆叉树
对⼀棵树有n个结点的⼆叉树按层序编号,所有的结点的编号从1-n 。如果这棵树所有结点和同样深度的满⼆叉树的编号为从1-n的结点位置相同,则这棵⼆叉树为完全⼆叉树
说⽩了,就是在满⼆叉树的基础上,在最后⼀层的叶⼦结点上,从右往左依次删除若⼲个结点,剩下的就是⼀棵完全⼆叉树
除了上述两种⼆叉树,还有堆、⼆叉排序树、平衡⼆叉树、红⿊树等
⼆叉树的存储
可以⽤vector数组或者链式前向星来存储。仅需在存储的过程中标记谁是左孩⼦,谁是右孩⼦即可。
- ⽐如⽤vector数组存储时,可以先尾插左孩⼦,再尾插右孩⼦;
- ⽤链式前向星存储时,可以先头插左孩⼦,再头插右孩⼦。只不过这样存储下来,遍历孩⼦的时候先遇到的是右孩⼦,这点需要注意。
但是,由于⼆叉树结构的特殊性,我们除了⽤上述两种⽅式来存储,还可以⽤符合⼆叉树结构特性的⽅式:分别是顺序存储和链式存储
顺序存储
顺序结构存储就是使⽤数组来存储。
在完全⼆叉树以及满⼆叉树的性质那⾥,我们了解到:如果从根节点出发,按照层序遍历的顺序,由1开始编号,那么⽗⼦之间的编号是可以计算出来的。那么在存储完全⼆叉树的时候,就按照编号,依次放在数组对应下标的位置上,然后通过计算找到左右孩⼦和⽗亲:
结点下标为i :
- 如果⽗存在,⽗下标为i/2 ;
- 如果左孩⼦存在,左孩⼦下标为i × 2 ;
- 如果右孩⼦存在,右孩⼦下标为i × 2 + 1
如果不是完全⼆叉树,也是可以⽤顺序存储。但是⾸先要先把这棵⼆叉树补成完全⼆叉树,然后再去编号。不然就⽆法通过计算找到左右孩⼦和⽗亲的编号
顺序存储结构⼀般只⽤于完全⼆叉树或满⼆叉树
链式存储
链式存储类⽐链表的存储,都有静态实现和动态实现的⽅式。
我们这⾥依旧只讲静态实现,也就是⽤数组模拟。动态实现,就是new和delete的⽅式
竞赛中给定的树结构⼀般都是有编号的,因此我们可以创建两个数组l[N]
,r[N]
,其中l[i]
表⽰结点号为i的结点的左孩⼦编号, r[i]
表⽰结点号为i的结点的右孩⼦编号。这样就可以把⼆叉树存储起来
案例:
题⽬描述:
有⼀个n(n<=10^6)个结点的⼆叉树。给出每个结点的两个⼦结点编号(均不超过n ),建⽴⼀棵⼆叉树(根节点的编号为1),如果是叶⼦结点,则输⼊0 0。
输⼊描述:
第⼀⾏⼀个整数n ,表⽰结点数。
之后n⾏,第i⾏两个整数l 、r,分别表⽰结点i的左右⼦结点编号。若l = 0则表⽰⽆左⼦结点,r = 0同理
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int l[N], r[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
// 存下 i 号结点的左右孩⼦
cin >> l[i] >> r[i];
}
return 0;
}
⼆叉树的遍历
深度优先遍历
不同于常规树的深度优先遍历,⼆叉树因其独特的性质可以划分成三种深度优先遍历:先序遍历,中序遍历,和后序遍历。其中,三种遍历⽅式的不同在于处理根节点的时机。
对于⼀棵⼆叉树⽽⾔,整体可以划分成三部分:根节点+左⼦树+右⼦树:
- 先序遍历的顺序为:根+左+右;
- 中序遍历的顺序为:左+根+右;
- 后序遍历的顺序为:左+右+根
测试⼀:
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 n;
int l[N], r[N];
void dfs1(int p)
{
if(p == 0) return;
// 先处理根节点
cout << p << " ";
// 左⼦树
dfs1(l[p]);
// 右⼦树
dfs1(r[p]);
}
void dfs2(int p)
{
if(p == 0) return;
dfs2(l[p]);
cout << p << " ";
dfs2(r[p]);
}
void dfs3(int p)
{
if(p == 0) return;
dfs3(l[p]);
dfs3(r[p]);
cout << p << " ";
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> l[i] >> r[i];
}
dfs1(1); // 先序遍历
cout << endl;
dfs2(1); // 中序遍历
cout << endl;
dfs3(1); // 后序遍历
cout << endl;
return 0;
}
宽度优先遍历
和常规的树的遍历⽅式⼀样,直接⽤队列帮助层序遍历即可
include <iostream>
#include <queue>
using namespace std;
const int N = 300;
int n;
int l[N], r[N];
void bfs()
{
queue<int> q;
q.push(1);
while(q.size())
{
auto p = q.front(); q.pop();
cout << p << " ";
// 左右孩⼦⼊队
if(l[p]) q.push(l[p]);
if(r[p]) q.push(r[p]);
}
cout << endl;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> l[i] >> r[i];
}
bfs();
return 0;
}
⼆叉树算法题
P1305 新二叉树 - 洛谷
- 建树:和常规的链式存储⽅式⼀致。
因为结点是字符,所以可以直接⽤ASCII码值当做下标来使⽤。⽐如 ‘a’ 直接映射成97 ,l[97]
⾥⾯就存着 ‘a’ 的左⼉⼦,r[97]
⾥⾯就存着 ‘a’ 的右⼉⼦,以此类推,建⽴⼆叉树。 - 先序遍历:根左右
#include <bits/stdc++.h>
using namespace std;
const int N = 300;
char root;
char l[N], r[N];
void dfs(char root)
{
if (root == '*') return;
cout << root;
dfs(l[root]);
dfs(r[root]);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n; cin >> n;
cin >> root;
cin >> l[root] >> r[root];
for (int i = 2; i <= n; i++)
{
char t; cin >> t;
cin >> l[t] >> r[t];
}
dfs(root);
return 0;
}
B3642 二叉树的遍历 - 洛谷
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int l[N], r[N];
int n;
void dfs1(int p)
{
if (p == 0) return;
cout << p << " ";
dfs1(l[p]);
dfs1(r[p]);
}
void dfs2(int p)
{
if (p == 0) return;
dfs2(l[p]);
cout << p << " ";
dfs2(r[p]);
}
void dfs3(int p)
{
if (p == 0) return;
dfs3(l[p]);
dfs3(r[p]);
cout << p << " ";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> l[i] >> r[i];
}
dfs1(1);
cout << endl;
dfs2(1);
cout << endl;
dfs3(1);
cout << endl;
return 0;
}
P4913 【深基16.例3】二叉树深度 - 洛谷
⼆叉树的⾼度=1+max(左⼦树的⾼度,右⼦树的⾼度);
因此,可以递归解决
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int l[N], r[N];
int n;
int dfs(int root)
{
if (!root) return 0;
int a = dfs(l[root]);
int b = dfs(r[root]);
return max(a, b) + 1;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> l[i] >> r[i];
}
cout << dfs(1) << endl;
return 0;
}
P1030 [NOIP 2001 普及组] 求先序排列 - 洛谷
已知「中序」和「后序」的基础上,如何求「先序」
- 根据后序遍历可知,最后一个元素A就是当前这棵树的根
- 根据中序遍历可知,根节点A会把整个序列分成两部分,左边是左子树,右边是右子树
- 那么我们就可以在后序遍历的序列中,也找出对应的左右子树
- 接下来根据划分情况,继续处理相应的子树
处理「左右⼦树」的⽅式与处理「原始序列」的⽅式⼀致,这样我们就可以⽤「递归」的⽅式求出先序序列
#include <bits/stdc++.h>
using namespace std;
string a, b;
void dfs(int l1, int r1, int l2, int r2)
{
//递归出口
if (l1 > r1) return;
//确定根节点
cout << b[r2];
int p = l1;
while (a[p] != b[r2]) p++; //p用来标记中序中根节点的位置
//划分左右子树
dfs(l1, p-1, l2, l2+p-l1-1);
dfs(p+1, r1, l2+p-l1, r2-1);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> a >> b;
dfs(0, a.size()-1, 0, b.size()-1);
return 0;
}
P1827 [USACO3.4] 美国血统 American Heritage - 洛谷
#include <bits/stdc++.h>
using namespace std;
string a, b;
void dfs(int l1, int r1, int l2, int r2)
{
if (l1 > r1) return;
//处理左右子树
//找根节点
int p = l1;
while (a[p] != b[l2]) p++;
//划分左右子树
dfs(l1, p-1, l2+1, l2+p-l1);
dfs(p+1, r1, l2+p-l1+1, r2);
//输出根节点
cout << b[l2];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> a >> b;
dfs(0, a.size()-1, 0, b.size()-1);
return 0;
}
P3884 [JLOI2009] 二叉树问题 - 洛谷
如何求x,y之间的距离
- 先从x向上爬,同时标记路径中,所有点到x的距离
- 接下来从y向上爬,当第一次遇到标记点时,更新结果
如何向上爬
int fa[N]
:fa[i]
表示i这个点的父亲是谁
如何标记当前点到x的距离
int dist[N]
:dist[i]
表示i这个点到x的最短距离
如何标记y到相遇点之间的距离
int len = 0
,如果y = fa[y]
,让len++,直到y走到相遇点或走到1为止
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n;
vector<int> edges[N];
int fa[N];
int dist[N];
int dfs(int u)
{
int ret = 0;
for (auto v : edges[u])
{
ret = max(ret, dfs(v));
}
return ret + 1;
}
int bfs()
{
queue<int> q;
q.push(1);
int ret = 0;
while (q.size())
{
int sz = q.size();
ret = max(ret, sz);
while (sz--)
{
int u = q.front(); q.pop();
for (auto v : edges[u])
{
q.push(v);
}
}
}
return ret;
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
int u, v; cin >> u >> v;
edges[u].push_back(v);
fa[v] = u;
}
cout << dfs(1) << endl;
cout << bfs() << endl;
int x, y; cin >> x >> y;
while (x != 1)
{
dist[fa[x]] = dist[x] + 1;
x = fa[x];
}
int len = 0;
while (y != 1 && dist[y] == 0)
{
len++;
y = fa[y];
}
cout << dist[y] * 2 + len << endl;
return 0;
}