利用树的结构来处理一些实际问题
其中一种应用为并查集
假设一个村子有n个人,编号为1,2,3,4,5,6,...,n,给定m对亲戚关系,然后去查询某两个人是不是亲戚关系,是的话输出YES,不是的话输出NO
2<=n<=100
那我们假设6为n
3为m
有以下三对有关系:①.4 3②.1 3③.3 2
并查集:处理一些不相交的集合的合并及查询。要根据给定的关系对,去合并集合
那么并查集主要有两个操作:一个合并,一个查询
那么根据并查集特征,我们可以使用树形结构对各数据进行保存
将每一个“人”设置为一个一个独立的节点,其实也可以视作只有根节点的n棵树
那么根据给定节点关系,我们对节点进行处理,存在关系的节点,合并在一起,而没有相关关系的,自然不在一棵树上,所以最终我们判定两者是否有亲戚关系转变为了,向上检索,直到检索到一个双亲结点指向自己的节点,检查其是否相同
具体合并操作一会在代码中介绍,因为构造关系树的方式多种多样,而构造出一棵不好的关系二叉树很有可能会变成单链表,使查找效率很烂
而解决的方法也比较直接,在创建好一棵树以后再进行递归,将根节点以下的节点都直接指向根节点,这样在查找的时候节点只剩下了一个,直接判断即可
下面我们使用顺序表数组进行查找即可,双亲表示法中节点中的数据就是父亲节点的下标
所以存储结构设置为:
//一个平平无奇的顺序表
int f[200];
提前设置好需要使用的全局变量:
int n,m,x;//n为人数,m为关系数,x为对
还有宏定义:
#include <stdio.h>
#include <stdlib.h>
在main函数中进行初始化顺序表:
int main()
{
int u,v;//u,v是进行查找的两个节点
printf("请输入人数(%%d):");
scanf("%d",&n);
for(int i=1;i<=n;i++){//从1开始编号
f[i] = i;
}
while(getchar!='\n');//清空缓冲区字符
printf("请输入关系数(%%d):");
scanf("%d",&m);
for(int i=1;i<=m;i++){
//输入有关系的下标节点
printf("第%d对关系为(%%d %%d):",i);
scanf("%d %d",&u,&v);
while(getchar!='\n');
}
return 0;
}
下面是合并代码,因为我们在main函数中需要进行对输入关系的处理。这里比较麻烦一些,因为给定关系是无序的,我们在对下标节点进行双亲结点的下标赋值时,会使本身为祖先的节点指向其他地方,从而无法正常进行寻根判断,所以我们需要先找u和v的根节点
void unionNode(int u,int v){
int fu = find(u);
int fv = find(v);
}
所以我们追加find函数用以找关系节点u、v的根节点,而在以下代码中我们写了三种形式的查找
第一种是非递归形式的循环,用以直接返回所对应树的根节点
第二种是递归形式,与第一种没什么两样,只是写法上不同
第三种是利用递归思想额外增设了路径节点的重新赋值,针对的节点是每次进入查找的节点,因为结合unionNode函数,其将后一节点v的根节点下标赋值给了前一节点的下标,明确了两者的连接关系。在main函数中使用时,我们只需要寻找一次即可找到两节点是否存在关系
//非递归形式
int find(int y){
while(f[y]!=y){
y = f[y];
}
return y;
}
//递归形式
int find(int y){
if(f[y]!=y){
return find(f[y]);
}
return y;
}
//递归形式,路径压缩优化O(1)
int find(int y){
//如果y的根节点和编号y不相同,则说明y不是根节点,继续进行查找
if(f[y]!=y){
return f[y] = find(f[y]);//将每个该树上的非根节点直接和根节点相连
}
return y;
}
继续进行unionNode函数的操作:
//联合函数,将有关系的节点设定在一个树中
void unionNode(int u,int v){
int fu = find(u);//返回u的根节点
int fv = find(v);//返回v的根节点
if(fu!=fv){
//如果两者的根节点不相同但是两者有关系,则让u的根节点处赋的值改为v的根节点,另其为同一根节点
f[fu] = fv;
}
}
继续编写main函数:
int main()
{
int u,v;//u,v是进行查找的两个节点
printf("请输入人数(%%d):");
scanf("%d",&n);
for(int i=1;i<=n;i++){//从1开始编号
f[i] = i;
}
while(getchar()!='\n');//清空缓冲区字符
printf("请输入关系数(%%d):");
scanf("%d",&m);
for(int i=1;i<=m;i++){
//输入有关系的下标节点
printf("(%d/%d)对关系为(%%d %%d):",i,m);
scanf("%d %d",&u,&v);
while(getchar()!='\n');
unionNode(u,v);
printf("(%d/%d)对关系产生后,每个节点的根节点为:\n",i,m);
for(int j=1;j<=n;j++){
printf("\t%d节点的根节点为:%d\n",j,find(j));
}
}
printf("请输入需要查找的关系数(%%d):");
scanf("%d",&x);
while(getchar()!='\n');
for(int i=1;i<=x;i++){
printf("(%d/%d)查找关系为(%%d %%d):",i,x);
scanf("%d %d",&u,&v);
while(getchar()!='\n');
int fu = find(u);
int fv = find(v);
if(fu==fv){
printf("YES\n");
}
else{
printf("NO\n");
}
}
return 0;
}
整体代码如下:
#include <stdio.h>
#include <stdlib.h>
//一个平平无奇的顺序表
int f[200];
int n,m,x;//n为人数,m为关系数,x为对
//递归形式,路径压缩优化O(1)
int find(int y){
//如果y的根节点和编号y不相同,则说明y不是根节点,继续进行查找
if(f[y]!=y){
return f[y] = find(f[y]);//将每个该树上的非根节点直接和根节点相连
}
return y;
}
//联合函数,将有关系的节点设定在一个树中
void unionNode(int u,int v){
int fu = find(u);//返回u的根节点
int fv = find(v);//返回v的根节点
if(fu!=fv){
//如果两者的根节点不相同但是两者有关系,则让u的根节点处赋的值改为v的根节点,令其为同一根节点
f[fu] = fv;
}
}
int main()
{
int u,v;//u,v是进行查找的两个节点
printf("请输入人数(%%d):");
scanf("%d",&n);
for(int i=1;i<=n;i++){//从1开始编号
f[i] = i;
}
while(getchar()!='\n');//清空缓冲区字符
printf("请输入关系数(%%d):");
scanf("%d",&m);
for(int i=1;i<=m;i++){
//输入有关系的下标节点
printf("(%d/%d)对关系为(%%d %%d):",i,m);
scanf("%d %d",&u,&v);
while(getchar()!='\n');
unionNode(u,v);
printf("(%d/%d)对关系产生后,每个节点的根节点为:\n",i,m);
for(int j=1;j<=n;j++){
printf("\t%d节点的根节点为:%d\n",j,find(j));
}
}
printf("请输入需要查找的关系数(%%d):");
scanf("%d",&x);
while(getchar()!='\n');
for(int i=1;i<=x;i++){
printf("(%d/%d)查找关系为(%%d %%d):",i,x);
scanf("%d %d",&u,&v);
while(getchar()!='\n');
int fu = find(u);
int fv = find(v);
if(fu==fv){
printf("YES\n");
}
else{
printf("NO\n");
}
}
return 0;
}
输入为:
6
3
4 3
1 3
3 2
3
1 2
4 2
3 5
输出为:
请输入人数(%d):6
请输入关系数(%d):3
(1/3)对关系为(%d %d):4 3
(1/3)对关系产生后,每个节点的根节点为:
1节点的根节点为:1
2节点的根节点为:2
3节点的根节点为:3
4节点的根节点为:3
5节点的根节点为:5
6节点的根节点为:6
(2/3)对关系为(%d %d):1 3
(2/3)对关系产生后,每个节点的根节点为:
1节点的根节点为:3
2节点的根节点为:2
3节点的根节点为:3
4节点的根节点为:3
5节点的根节点为:5
6节点的根节点为:6
(3/3)对关系为(%d %d):3 2
(3/3)对关系产生后,每个节点的根节点为:
1节点的根节点为:2
2节点的根节点为:2
3节点的根节点为:2
4节点的根节点为:2
5节点的根节点为:5
6节点的根节点为:6
请输入需要查找的关系数(%d):3
(1/3)查找关系为(%d %d):1 2
YES
(2/3)查找关系为(%d %d):4 2
YES
(3/3)查找关系为(%d %d):3 5
NO
我们将节点下标与节点值相同的点强制作为根节点进行判断,最后都可推导出,不管如何更改值,或者处理节点关系,都不会混淆节点内容
可以看到如果是同一棵树,那么只需要赋同一个值进行数组元素判断即可,基本逻辑就是将后值赋给前值,前值如果以前是别的节点的后值,那么在本次修改中一并会自动变换数值
这次我将特殊强调结果,因为在main中我特地增加了每次添加关系后输出每个节点的根节点情况,以此方便更深入理解代码的含义,我们会注意到find函数的赋值起到了连带关系的作用,每次的修改虽然可能会使树根节点发生转变,但是这不影响关系的查询,目的性很强,额外注意的一点就是一个数组的赋值修改后,是指该地址上的数值修改了,那么之前赋过相应地址值的数组元素中的值也会发生改变,所以该算法还巧妙的利用了数组的性质
树、二叉树和森林的转换
本小节不涉及代码,只需要学会转换即可,即学会孩子兄弟存储结构即可
因为在涉及树的存储的时候,我们有双亲存储、孩子存储、孩子兄弟存储,孩子兄弟存储又很好的表示了一个树中的二元关系,基本思路为,将树中处于同一层的树节点中的第一个节点设置为其上一层根节点的左孩子,而将右孩子的位置设置为同层的的剩余兄弟们,左孩子右兄弟
上述图片十分直观,而森林也相同
总的思路就是左孩子右兄弟
如果不清楚还可以去王卓老师的数据结构与算法课中找相应的内容去听一下,容易理解