二叉树
文章目录
声明
本篇是数据结构系列的第四章,详解二叉树的储存、建立、操作、模板、经典例题。
本系列基于代码源wls的数据结构系列课程,主要内容包括栈、队列、链表、树、堆、单调栈、单调队列、哈希、字典树、并查集持续更新,欢迎关注。
所有例题均可在Home - Daimayuan Online Judge的数据结构课程题单中提交。
完全二叉树的储存、建立
数组模拟
-
以数组[1]位置为根节点建立二叉树
-
数组[k]的左儿子[2k],右儿子[2k + 1],父亲结点[k/2]
递归建树
建树逻辑:
//build以k为根节点的树
void Build(int k) {
//添加数据
UpdateData(k);
//若子节点存在
Build(2t);
Build(2t + 1);
}
如果用数组模拟的方法建立非完全二叉树,会出现空间浪费的情况,因为需要保证左儿子是2k,右儿子是2k+1,数组里可能会存放“空结点”,所以是否使用这种方法建树取决于具体情况
为了避免这种情况,可以为所有已存在的结点编号,然后用结构体记录每个结点的值以及左儿子、右儿子、父结点的编号。
struct TreeNode{
int value;
int l, r, fa;
}a[10001];
这种方法坏处是每个结点占用空间大。所以使用哪种方式还是需要看具体情况
指针存储
与链表类似
struct TreeNode{
int value;
TreeNode *l, *r, *fa;
TreeNode(int x){value = x;}//构造函数
};
基本操作
新建节点并初始化
TreeNode *p;
p = new TreeNode(x);
其实就是
TreeNode *p = new TreeNode(x);
插入子节点
//把p变成fa的左/右儿子,flag = 0就是左,1就是右
void insert(TreeNode *fa, TreeNode *p, int flag) {
if(!flag)
fa -> l = p;
else
fa -> r = p;
p -> fa = fa;
}
TreeNode *p = new TreeNode(x);
insert(fa, p, flag);
删除后续会讲
遍历
区分先中后可以看输出语句在递归的哪个位置,规律性极强
先序
输出在前,即先访问根,再访问左子树,再访问右子树
void PreOrder(TreeNode *p) {
cout << p -> value << endl;
if(p -> l) PreOrder(p -> l);
if(p -> r) PreOrder(p -> r);
}
中序
中序输出语句在中间,即先访问左子树,再访问根,再访问右子树。非常的形象
void InOrder(TreeNode *p) {
if(p -> l) PreOrder(p -> l);
cout << p -> value << endl;
if(p -> r) PreOrder(p -> r);
}
后序
原理一样的
void PostOrder(TreeNode *p) {
if(p -> l) PreOrder(p -> l);
if(p -> r) PreOrder(p -> r);
cout << p -> value << endl;
}
层级遍历(bfs序)
用队列来做。
每次遍历队列front,处理完后将其两个儿子加入队尾
例:
先放入结点1,处理完后将结点1的两个子节点2、3塞入队尾,此时队列里有:1、2、3,然后将1出队。再处理队首结点即结点2,处理完后将它的两个子节点4、5放入队尾,此时队列里有:2、3、4、5,然后将结点2出队,如此循环往复直到队列为空
TreeNOde *q[N];//队列
void bfs(TreeNode *root) {
int l = 1, k = 0;//l指向队首、k指向队尾
//塞入根节点
q[++k] = root;
while(l <= k) {
TreeNode *p = q.[front];
l++;//出队
cout << p -> value << endl;//处理
//塞入左右儿子
if(p -> l) q[++rear] = p -> l;
if(p -> r) q[++rear] = p -> r;
}
}
计算各结点深度
只需要在结点的定义里加上变量d来表示结点的的深度
struct TreeNode {
int value;
int d;
TreeNode *l, *r ,*fa
TreeNode(int x){value = x;}//构造函数
}
只需要在遍历的时候加上“当前结点d等于父亲结点d+1”即可
例如:
层序遍历时:
只需要加上两句赋值语句即可
TreeNOde *q[N];//队列
void bfs(TreeNode *root) {
int l = 1, k = 0;//l指向队首、k指向队尾
//塞入根节点
q[++k] = root;
while(l <= k) {
TreeNode *p = q.[front];
//左右儿子深度等于父亲深度加一即可
p -> l -> d = p -> d + 1;
p -> r -> d = p -> d + 1;
l++;//出队
cout << p -> value << endl;//处理
//塞入左右儿子
if(p -> l) q[++rear] = p -> l;
if(p -> r) q[++rear] = p -> r;
}
}
前序遍历(中序、后序也一样):
只需要在递归前加上赋值语句即可
void perOrder(TreeNode *p){
cout << p -> value << end;
if(p -> l) p -> l -> d = p -> d + 1, perOrder(p -> l);
if(p -> r) p -> r -> d = p -> d + 1, perOrder(p -> r);
}
例1:遍历完全二叉树
简单的遍历即可,由于二叉树结点的值就是它的编号,而且编号就1到n,所以根本不需要建树,遍历时输出编号值即可
三种遍历
还是记住前、中、后序就是指输出位置在前、中、后
写成inline是减少重复调用的时间,只是个习惯
inline void preOrder(int t){
printf("%d ", t);
if(t + t <= n) preOrder(t + t);
if(t + t + 1 <= n) preOrder(t + t + 1);
}
inline void inOrder(int t){
if(t + t <= n) inOrder(t + t);
printf("%d ", t);
if(t + t + 1 <= n) inOrder(t + t + 1);
}
inline void postOrder(int t){
if(t + t <= n) postOrder(t + t);
if(t + t + 1 <= n) postOrder(t + t + 1);
printf("%d ", t);
}
主函数
直接调用即可
int main() {
scanf("%d", &n);
preOrder(1);
printf("\n");
inOrder(1);
printf("\n");
postOrder(1);
return 0;
}
例2:遍历一般二叉树
只需要建树、遍历即可。一般二叉树一般用指针来存。此处使用数组存放结点,a[i]表示编号为i的结点,然后用int指示左儿子、右儿子、父节点的下标。这其实也是一种链式存储结构,只不过调用起来把指针的->换成了.,方便一些
由于题目要求输出编号,所以没有value变量
struct TreeNode {
int l, r, fa;
}a[1025];
这样的方式调用起来就是a[i].l这种类型,会比用指针(p -> l)写起来舒服一些
建树
读入x、y,x就是i的左儿子编号,y就是i的右儿子编号。如果x不为0,就把a[i]的l指向x,a[x]的fa指向i,y也一样。
for(int i = 1; i <= n; i++){
int x, y;
scanf("%d%d", &x, &y);
if(x) a[i].l = x, a[x].fa = i;
if(y) a[i].r = y, a[y].fa = i;
}
之后调用三种遍历,这个根上一题一样
preOrder(1);
printf("\n");
inOrder(1);
printf("\n");
postOrder(1);
return 0;
遍历
有少许不同,这里a[i]的左儿子变成了a[i].l,右儿子变成了a[i].r,稍作修改即可
inline void preOrder(int t){
printf("%d ", t);
if(a[t].l) preOrder(a[t].l);
if(a[t].r) preOrder(a[t].r);
}
inline void inOrder(int t){
if(a[t].l) inOrder(a[t].l);
printf("%d ", t);
if(a[t].r) inOrder(a[t].r);
}
inline void postOrder(int t){
if(a[t].l) postOrder(a[t].l);
if(a[t].r) postOrder(a[t].r);
printf("%d ", t);
}
例3:二叉树的最近公共祖先
思路
最简单的思路就是记录u和v结点到根的路径,然后逐一比对输出第一次相等的那个结点。
编码
数据处理
题目不涉及结点的值,结构体里不需要value
struct node {
int l, r, fa;
} a[1001];
int n, c[1001], d[1001];
用c、d记录u和v结点到根结点的路径
读入二叉树的操作与之前相同:
for (int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
if (x)
a[i].l = x, a[x].fa = i;
if (y)
a[i].r = y, a[y].fa = i;
}
运算
“当u不是根节点时,将u压入c数组,u=a[u].fa”
然后将根节点压入c数组
int l1 = 0;
while (u != 1)
c[++l1] = u, u = a[u].fa;
c[++l1] = 1;
同理,对v进行相同的操作
int l2 = 0;
while (v != 1)
d[++l2] = v, v = a[v].fa;
d[++l2] = 1;
最后从后往前遍历c、d数组,输出最后一个相等的位置就可以了
int x = 0;
for (int i = l1, j = l2; i && j; --i, --j) {
if (c[i] == d[j])
x = c[i];
else
break;
}
printf("%d\n", x);
整体代码
虽然看起来挺多的,核心逻辑只有上述加粗的那一句话
#include <bits/stdc++.h>
using namespace std;
struct node {
int l, r, fa;
} a[1001];
int n, c[1001], d[1001];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
if (x)
a[i].l = x, a[x].fa = i;
if (y)
a[i].r = y, a[y].fa = i;
}
int u, v;
scanf("%d%d", &u, &v);
int l1 = 0;
while (u != 1)
c[++l1] = u, u = a[u].fa;
c[++l1] = 1;
int l2 = 0;
while (v != 1)
d[++l2] = v, v = a[v].fa;
d[++l2] = 1;
int x = 0;
for (int i = l1, j = l2; i && j; --i, --j) {
if (c[i] == d[j])
x = c[i];
else
break;
}
printf("%d\n", x);
return 0;
}
更简单的思路
给出两个结点编号u、v,我们可以直接求出这两个结点的深度。
那可以先将深度更深的那个结点向上找它的父结点直到两个结点深度相同。然后在各自向上找它的父节点,如果找到的结点相同,那么该结点就是u和v最近公共组先
即:先转化到同一深度,再同时向上找父节点,看找到的父节点何时相同
编码
数据处理
额外记录一下深度,然后用层级遍历求出每个结点的深度,q就是那个队列
struct node {
int l, r, fa;
int depth;
} a[1025];
int n, q[1025];
还是跟之前一样读入二叉树
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
if (x)
a[i].l = x, a[x].fa = i;
if (y)
a[i].r = y, a[y].fa = i;
}
逻辑
层级遍历求深度,这个是个板子
复述一下,就是先塞入根节点,每次把队首的子节点加入队尾,被加入的那个结点的深度等于队首深度加一
int front = 1, rear = 0;
q[++rear] = 1;
a[1].depth = 1;
while (rear >= front) {
int p = q[front];
++front;
if (a[p].l)
q[++rear] = a[p].l, a[a[p].l].depth = a[p].depth + 1;
if (a[p].r)
q[++rear] = a[p].r, a[a[p].r].depth = a[p].depth + 1;
}
读入u,v,为了方便运算,让u变成深度最大的那个
int u, v;
scanf("%d%d", &u, &v);
if (a[u].depth < a[v].depth)
swap(u, v);
将u向上移至与v同深度,其实也就是移动深度之差次
int x = a[u].depth - a[v].depth;
for (int i = 1; i <= x; ++i)
u = a[u].fa;
最后同时找父节点,相等就停止
while (u != v)
u = a[u].fa, v = a[v].fa;
printf("%d", u);
整体代码
核心逻辑也只有“移至同层级,同时找父节点,相同即退出”这一句话
#include <bits/stdc++.h>
using namespace std;
struct node {
int l, r, fa;
int depth;
} a[1025];
int n, q[1025];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
if (x)
a[i].l = x, a[x].fa = i;
if (y)
a[i].r = y, a[y].fa = i;
}
int front = 1, rear = 0;
q[++rear] = 1;
a[1].depth = 1;
while (rear >= front) {
int p = q[front];
++front;
if (a[p].l)
q[++rear] = a[p].l, a[a[p].l].depth = a[p].depth + 1;
if (a[p].r)
q[++rear] = a[p].r, a[a[p].r].depth = a[p].depth + 1;
}
int u, v;
scanf("%d%d", &u, &v);
if (a[u].depth < a[v].depth)
swap(u, v);
int x = a[u].depth - a[v].depth;
for (int i = 1; i <= x; ++i)
u = a[u].fa;
while (u != v)
u = a[u].fa, v = a[v].fa;
printf("%d", u);
return 0;
}
例4:二叉树子树和1
思路
数据较小,可以遍历以i为根的子树的权值和
编码
数据处理
value记录权值,cnt记录权值和
struct node {
int l, r, fa;
int value;
} a[1025];
int n, cnt;
读入
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
if (x)
a[i].l = x, a[x].fa = i;
if (y)
a[i].r = y, a[y].fa = i;
}
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i].value);
逻辑
dfs,每次把value加到cnt里
inline void order(int t) {
cnt += a[t].value;
if (a[t].l)
order(a[t].l);
if (a[t].r)
order(a[t].r);
}
输出每个结点的cnt,记得开始dfs之前或者dfs结束之后把cnt归0
for (int i = 1; i <= n; ++i) {
cnt = 0;
order(i);
printf("%d ", cnt);
}
整体代码
#include <bits/stdc++.h>
using namespace std;
struct node {
int l, r, fa;
int value;
} a[1025];
int n, cnt;
inline void order(int t) {
cnt += a[t].value;
if (a[t].l)
order(a[t].l);
if (a[t].r)
order(a[t].r);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
if (x)
a[i].l = x, a[x].fa = i;
if (y)
a[i].r = y, a[y].fa = i;
}
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i].value);
for (int i = 1; i <= n; ++i) {
cnt = 0;
order(i);
printf("%d ", cnt);
}
return 0;
}
例5:二叉树子树和2
n的范围变成了1到1000000
思路
结点i的子树权值和=左子树权值和+右子树权值和+i的权值
所以在求根节点子树和的时候其实已经把所有结点的子树和都求出来了
用数组存储每个结点的子树和,输出数组即可
编码
数据处理
v[i]代表结点i的子树权值和
struct node {
int l, r, fa;
int value;
} a[1000001];
int n, v[1000001];
读入二叉树,跟之前一模一样
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
if (x)
a[i].l = x, a[x].fa = i;
if (y)
a[i].r = y, a[y].fa = i;
}
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i].value);
逻辑
先令x等于a[t]的权值,如果左子树存在,就把左子树权值和加到x里,右子树同理,然后把x计入v里,返回x即可
int solve(int t) {
int x = a[t].value;
if (a[t].l)
x += solve(a[t].l);
if (a[t].r)
x += solve(a[t].r);
v[t] = x;
return x;
}
调用函数并输出
solve(1);
for (int i = 1; i <= n; ++i)
printf("%d ", v[i]);