概念
并查集虽然是个进阶数据结构,但是其思想却很简单。它主要用来维护集合以及处理部分图相关的题目(相对来说很多题目都可以抽象成一个图论题)。它支持Union、Find两种操作:
合并
:合并两个集合查找
:判断两个元素是否在同一个集合中
并查集定义
维护这个操作只需要一个数组来保存每一个元素对应的父节点:
int father[N];
father[i]
代表第i
个元素的父节点,比如说father[1]=2
,表示元素1的父亲是元素2。对一个集合来说,其根节点也就是祖宗节点存在这个特点:father[i]==i
。如下图,2是3和4的父节点,1是2的父节点。5是6的父节点。
并查集基本操作
基本操作也就3个,分别如下:
初始化
初始化很简单,只需要把father数组的每一个值赋给自己就行:
for(int i=1;i<=N;i++)
{
father[i]=i;
}
查找
查找所要实现的功能是找到x
元素的根节点。对于查找而言,其实就是一个不断重复的过程,比如要查找一个节点5的最终父节点:
- 首先判断当前节点是否是根节点:要满足
father[x]==x
,不满足就循环1和2 x=father[x]
;把father[x]
赋值给x
,递推就可以了
int findFather(int x)
{
while(x!=father[x])
{
x=father[x];
}
return x;
}
这个过程类似于链表的查找尾结点。因为是遍历,其时间复杂度是0(n)
合并
对于合并而言,就是把两个集合合并成一个,也就是说把两个集合的父节点设置为同一个。遵循这个思路:
- 查询
A
的根节点 - 查询
B
的跟节点 - 判断两个节点是否相等,不相等就把
B
的值赋值给father[A]
void Union(int a,int b)
{
int father_a=findFather(a);
int father_b=findFather(b);
if(father_a!=father_b)
{
father[father_a]=father_b;
}
}
基于上面查找操作的合并操作的时间复杂度也是0(n)
,但是可以优化,如下:
路径压缩
显然,查找操作的时间复杂度有点高,极端情况下需要遍历,0(n)
的时间复杂度。事实上查找函数只是为了找跟节点,链式查找没有必要,中间信息没有作用,不如直接把中间的所有节点的父节点全部设置为根节点。如下:
这样,在查找的时候,其时间复杂度就几乎变成了O(1)
递推实现:
int findFather(int x)
{
//1.首先找到x的根节点
int a=x; //留存等下要用,x会发生变化
while(x!=father[x])
{
x=father[x];
}
//目前x保存着根节点的值
while(a!=father[a]) //再来一次从头遍历
{
int z=a; //同理也是为了留存
a=father[a]; //a等于父节点
father[z]=x; //把上一个节点的父节点赋值成x,也就是跟节点
}
//2.把之前所有节点的父节点设置成根节点
}
递归写法:
int findFather(int x)
{
if(x==father[x]) return x;
else
{
int fa=findFather(x);
father[x]=fa;
return fa;
}
}
总结
并查集是一个维护对集合进行查找、合并的数据结构,经过优化后,其查找与合并的时间复杂度为O(1)