在上一节中,我们对并查集进行了一个结构上的优化,然而,我们发现在数据量很大时,貌似效率并没有被提升很多,因此,我们今天就来讨论一下并查集的另一种优化方式:基于Size的优化。
在上一节中,我们对于两个集合的合并,貌似是随意的进行的,然而,这样会引发一些极端的情况出现,例如,在下图中:
如果我们需要把4和9所在的集合合并,对于原来的代码逻辑,我们总是把第一个集合的根元素的父节点指向另一个集合的根元素,也就是parent[8]=9,如下图所示:图一
但是,如果我们把9和4所在的集合合并,对于原来的代码逻辑,也就是parent[9]=4。如下图所示:图二
我们看到,虽然从逻辑上Union(4,9)和Union(9,4)的操作是一样的,并且并不影响其他方法的实现,然而,我们会发现,两种情况产生的树形是不一样的。对于我们最开始的实现的Unionelements来说,我们总是把第一个元素所在的根的父亲节点指向第二个集合的根节点,这样没有规则的随意合并极易造成像图二这样的层数非常多的树,导致在做find查询时会大大降低效率,特别是在数据量非常大的时候,这样的劣势体现得就更加的明显。
因此,我们必须引入一种机制来指引两个集合的合并,我们不应该固定的把第一个集合的根元素的父亲节点指向另一个集合的根元素。因此,我们可以引入另外一个叫做Size的数组,专门用来记录每一个集合都有多少个元素,然后在进行联合操作的时候,我们借用Size数组来查询需要合并的两个集合当中哪一个集合中的元素个数比较少,我们就可以把Size数小的集合并入Size数大的集合当中去。这样,就能大大的减少因为合并造成树的层数过高的现象,提高find效率。
并查集的基础结构:
int* parent;//parent指针指向一个专门用来记录元素父亲元素的指针
int count;//计录集合中元素的数量
int *size;
并查集初始化:
for(int i=0;i<n;i++){
parent[i]=i;
size[i]=1;//最开始所有的元素都指向自己,每一个元素都是根元素,每一个集合只有一个元素
}
合并两个元素所在的集合:
//合并两个元素所在的集合
void unoinelements(int p,int q){
int proot=find(p);//找出p元素位于的集合的根元素
int qroot=find(q);//找出q元素位于的结合的根元素
if(proot==qroot){//如果两个元素的根元素都为同一个元素,则它们已经在同一个集合当中了
return;
}
else{//两个元素在不同的集合当中
if(size[proot]<=size[qroot]) {//以p元素为根的集合的元素个数小于等于以q元素为根的集合的元素个数
parent[proot] = qroot;//p所在集合的根元素proot的父亲指针指向q元素所在的集合的根元素
size[qroot]+=size[proot];//以qroot为根的集合元素数量增加了,需要更新
}
else{
parent[qroot]=proot;//q元素所在的集合并入p元素所在的集合当中
size[proot]+=size[qroot];//更新size数组
}
}
}
我们可以惊奇的发现,UF3与UF1,UF2的效率完全不在一个数量级上,在对100万的数据上进行200万次操作,UF3仅仅只用了0.1S,效率大大的提高!
如需访问此版本所有的源代码,请点击此处移步我的Github代码仓库。