在计算机科学中,并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(union-find algorithm)定义了两个用于此数据结构的操作:
- Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
- Union:将两个子集合并成同一个集合。
并且,在图的相关算法中需要使用到并查集来实现一些有关于图的算法,并查集相对于二叉树和堆来说,是一种很不一样的树形结构。
对于一组数据来说,并查集支持以下操作:
对于一组数据,主要支持两个动作:
•
union( p , q )
•
find( p )
用来回答一个问题
•
isConnected
( p , q )
我们把一组数据用数组来表示,数组的索引用来表示元素,索引对应的值表示该元素所位于的集合的编号,如下图所示:
其中,id[0]表示0元素位于的集合编号为0,id[1]表示1元素位于的集合的编号为1,id[2]表示2元素位于的集合的编号为0,因此我们可以发现,0元素与2元素所在的集合编号都为0,也就是说0元素和2元素在同一个集合当中,而1元素所在的集合的编号为1,因此1元素与0,2元素都不要在同一个集合当中,剩下的元素都可以以此类推。
我们要实现一个并查集的类,基础数据为:
int *id;//一个数组用来存储每一位相应的集合id
int count;//记录数据个数
相应的构造函数为:
UnionFind1(int n){//构造函数
id=new int[n];//构造id数组
count=n;
for(int i=0;i<n;i++){
id[i]=i;//初始化,每一个id[i]都指向自己单独的一个集合
}
}
因此,基于这种结构,我们如果要查询一个元素位于哪个集合,只要使用id[x]就可以查出x元素位于的集合的编号。我们称这样的查询为quickfind,其时间复杂度只有O(1)。
find函数实现如下:
//查找元素P所对应的集合编号
int find(int p){
assert(p>=0&&p<count);//确保查询的范围
return id[p];//返回p元素所属的集合编号
}
对于isconnected()函数,我们的实现就更加的简单了。如果两个函数在同一个集合中的话,那么我们通过id[x]查询出来的集合编号就应该是相等的了,根据这个原因,我们可以很容易的写其代码:
//查询两个元素是否在同一个集合中
bool isconnected(int p,int q){
return id[p]==id[q];
}
接下来我们在看一下对于并查集最难的Union操作是如何实现的:
我们想一想,如果需要使两个元素所位于的集合并起来,则他们的集合编号应该都一样才能表示他们在同一个集合了,因此,我们的只需要把任意一个组的所有组员对应的集合编号都改为另一个组的集合编号就好了,然而,每次执行Unionc操作我们都需要遍历一遍数组的所有元素然后更改其集合编号,因此此时的时间复杂度为O(n),下面是Union的具体代码:
// 合并元素p与元素q所在的两个集合
void unionElements(int p,int q){
int pID=find(p);//先找出p,q元素各自所在的集合的id
int qID=find(q);
if(pID==qID){//p,q的集合编号id一样,则p,q已经在同一个集合中了
return;
}
else{//p,q两者不在同一个集合当中,需要进行合并操作
for(int i=0;i<count;i++){//遍历数组所有的元素一遍
if(id[i]==qID){//把所有处于qID集合的元素的id改为pID,使得两个集合合并为一个集合qID集合
id[i]=pID;
}
}
}
}
为了体现后序对UnionFind的优化带来的效果,我们另外写了一UnionFindTestHelper文件来测试并查集相应的效率,以下是相应的代码:
using namespace std;
//测试并查集的辅助函数
namespace UnionFindTestHelper {
void TestUF1(int n) {//测试第一版本的并查集辅助函数
srand(time(NULL));//初始化随机种子
UF1::UnionFind1 uf = UF1::UnionFind1(n);//实例化UF1并查集
time_t starttime = clock();//记录起始时间
//进行n次操作,每次随机选择两个元素进行合并操作
for (int i = 0; i < n; i++) {
int a=rand()%n;//随机选取a,b两个随机数
int b=rand()%n;
uf.unionElements(a,b);
}
//在进行n次查询操作
for(int i=0;i<n;i++){
int a=rand()%n;//随机选取a,b两个随机数
int b=rand()%n;
uf.isconnected(a,b);
}
time_t endtime=clock();//记录下所有操作结束时的时间点
//打印输出耗时
cout<<"UF1 have done:"<<2*n<<" performances,use time:"<<double(endtime-starttime)/CLOCKS_PER_SEC<<"s"<<endl;
}
}
我们接下来看看排序100000个元素所需要的时间:
int main() {
int n=100000;
UnionFindTestHelper::TestUF1(n);
return 0;
}
结果:
UF1 have done:200000 performances,use time:8.373s
如需访问全部源代码请点击此处。