在上一节中,我们讨论了并查集的简单实现方法,然而我们却发现并查集的效率好像并不是很高,因此,计算机科学家想出了另外一种方法来实现并查集。
在新的实现思路中,我们把每一个元素看做是一个节点,该节点只有一个指向其父亲的指针,也就是说parent[i]=j的意思就是i元素的父亲为j元素,因为 i 元素的父亲指针就是指向 j 节点的,如果一个元素没有父亲了,那么它的父亲指针就指向自己,这也是作为根节点的一个标志。
如上图所示,对于2元素,他的父亲指针是指向自己的,也就是说2元素是这个集合的根,我们可以把结合理解为一个家族,位于同一个人家族的人他们的祖宗肯定是同一个人的,类比成并查集的话,也就是所位于同一个集合中的元素其根节点都是同一个节点的,在上图中,最开始(5,6,7)为一个集合,(2,3)为一个集合,(1)为一个集合,
如果我们需要把1元素与3元素各自相应所在的集合合并的话,我们该怎么办呢?
我们可以类比成两个家族,如果两个不同的家族要合并成为一个家族的话,也就是两个家族的祖先必须是一样的,因此,我们只需要任意选一个家族的祖先,让该祖先的父节点由指向其自己改为指向另一个家族的祖先,这样两个家族的所有的成员的祖先就是同一个人元素了,我们也完成了两个家族的合并。
转换成并查集来说,就是把一个集合的根节点的父节点指向其另一个集合的根节点。这样就完成了Union操作。
对于上图来说,要把三个集合合并成为一个集合,我们只需要把5元素(根元素)的父亲指针指向2元素(根元素),把1元素(根元素)的父亲指针指向2元素(根元素)就好了。
如上图所示,这是一个通过上述方法已经完成了合并操作的并查集,通过partent[i]可以查到 i 元素的父亲节点,然后parent[父亲节点]又能够了解到父亲节点的父亲节点,一直到根元素位置。
另外:根元素的父亲指针指向其自己。
让我们来看一下具体的代码实现:
//找出元素p位于的集合的编号
//即返回根元素
int find(int p){
assert(p>=0&&p<count);//防止数组越界
while(p != parent[p]){//如果p元素的父亲指针指向的不是自己,说明p并不是集合中的根元素,还需要一直向上查找
p=parent[p];//p变成其父亲
}
return p;//经过while循环后,p=parent[p],一定是一个根节点,我们返回即可
}
//判断两个元素是否位于同一个集合当中
bool isconnected(int p,int q){
return find(p)==find(q);//p,q元素的根节点一样,则位于同一个集合当中
}
//合并两个元素所在的集合
void unoinelements(int p,int q){
int proot=find(p);//找出p元素位于的集合的根元素
int qroot=find(q);//找出q元素位于的结合的根元素
if(proot==qroot){//如果两个元素的根元素都为同一个元素,则它们已经在同一个集合当中了
return;
}
else{
parent[proot]=qroot;//否则把一个根节点的父亲指针指向另一个根节点
}
}
接下来我们测试一下第二版本的并查集执行2万次操作的时间对比:
我们发现,UF2的效率要高出UF1,我们接下来测试20万次操作的时间对比:
貌似当数据量很大时,UF2并没有体现出很大的优化力度。
因此,我们需要对这一版本的并查集还要继续优化。
第二版本的完整源代码请点此此处访问。