您的位置:首页 > 其它

算法学习——并查集

2018-03-05 15:28 417 查看

前言

大家肯定有听说过社交网络里面的六人理论吧,说是可以通过六个人的联系认识世界上的任意一个人。比如我想认识一下机械系的系花,我先找到机械系的朋友,然后通过朋友介绍认识。这样可以发现我们的社交圈子其实是有部分重叠的。当然也有可能是我的圈子太小,根本没有什么朋友认识那个圈子里面的人,也有很大可能是机械系花404。我们之间是否存在联系,即连接问题,这类问题往往可以通过并查集实现。

并查集

原理

并查集,顾名思义,我们可以对集合进行并和查两种操作,即合并两者之间的联系或者查找两者之间是否存在联系。

上面那个例子中人的姓名和关系我们就可以使用一个数组id来进行标记,索引i表示人名,id[i]则表示联系。一开始赋值的时候,我们先令
id[i] = i;
表示每一个元素都和自己有关,后面需要建立联系时修改一方的id值,使它指向另一个元素。最终通过查找id,即可判断是否存在联系。



代码实现

可以看到我们的查询代码很简短,它的速度也是很快的,时间复杂度为O(1)级别的。

namespace UF1{

class UnionFind{

private:
int* id;//表示联系
int count;//数组数

public:
UnionFind(int n){
count = n;
id = new int
;
for(int i = 0; i < n; i++)
id[i] = i;
}

~UnionFind(){
delete[] id;
}

int find(int p){
assert(p >= 0 && p < count);
return id[p];
}

bool isConnected(int p, int q){
return find(p) == find(q);
}

void unionElements(int p, int q){

int pID = find(p);
int qID = find(q);

//如果已经相等
if(pID == qID)
return;

for(int i = 0; i < count; i++)
if(id[i] == pID)
id[i] = qID;
}
};

}


优化

优化1.0

虽然我们的查询是很快的,但是我们的合并却仍有可以优化的地方。

当我们合并两个元素时,总是让后一个指向前一个,但是这样话,树的高度将会越来越高,我们的查询效率也会随之降低。那我们有什么办法降低树的高度呢?当然有,我们可以将元素直接连接到根节点处,就如下图所示,直接将9连接到8处即可,这样通过查找根节点是否相同,我们就可以判断两者是否连接。



代码如下:

我们不断查找父节点的父节点,直到
parent[i] == i
就表示找到了根节点。

class UnionFind{
private:
int* parent;
int count;
public:
UnionFind(int count){
parent = new int[count];
this->count = count;
for(int i = 0; i < count; i++)
parent[i] = i;
}

~UnionFind(){
delete[] parent;
}

//找到从属的根节点
int find(int p){
assert(p >= 0 && p < count);
//直到p成为根节点
while(p != parent[p])
p = parent[p];

return p;
}

bool isConnected(int p, int q){
return find(p) == find(q);
}

void unionElements(int p, int q){

int pRoot = find(p);
int qRoot = find(q);

if(pRoot == qRoot)
return;

parent[pRoot] = qRoot;
}
};


优化2.0

通过前面的代码我们可以看到,我们总是将后一个指向前面一个,但是有时候让前面一个指向后面一个反而树的高度更小。其实这里有两种优化,一种是通过统计以i为根的元素个数sz,还有一种是统计以i为根的树的高度rank。sz和rank大的优先作为根节点。

同时通过下面两张图片我们也可以清晰地看出,两者相比而言,rank更具合理性。

sz图示:



rank图示:



sz代码:

class UnionFind{
private:
int* parent;
int* sz; //sz[i]表示以i为根的集合中元素的个数
int count;

public:
UnionFind(int count){
parent = new int[count];
this->count = count;
for(int i = 0; i < count; i++){
parent[i] = i;
sz[i] = 1;
}

}

~UnionFind(){
delete[] parent;
delete[] sz;
}

//找到从属的根节点
int find(int p){
assert(p >= 0 && p < count);
//直到p成为根节点
while(p != parent[p])
p = parent[p];

return p;
}

bool isConnected(int p, int q){
return find(p) == find(q);
}

void unionElements(int p, int q){

int pRoot = find(p);
int qRoot = find(q);

if(pRoot == qRoot)
return;

if(sz[pRoot] < sz[qRoot]){
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}
else{
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}

}
};


rank的话仅仅是合并处有所不同:

void unionElements(int p, int q){

int pRoot = find(p);
int qRoot = find(q);

if(pRoot == qRoot)
return;

if(rank[pRoot] < rank[qRoot]){
parent[pRoot] = qRoot;
}
else if(rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ //rank[qRoot] == rank[pRoot]
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}

}


优化3.0

很多时候评价一个算法是否优秀,我们不应该只看它一方面的表现,还得看它在很多特殊的情况下,性能是否还能保持稳定。在并查集的测试中,我们的测试都是随机的,但是当我们的样例是一个单方面依赖的树,那么搜索的效率就回下降很多。这时候,计算机科学家就发明了一种路径压缩的算法。



路径压缩算法采用两步一跳的方式,同时在搜索的过程中,将原有的结构进行修改压缩。这里大家可能会担心会不会跳出根节点,这点不用担心,因为根节点的父节点还是自己,所以并不会跳出。

路径压缩完成之后,当我们进行下一次操作时,树的高度就大大降低了。



class UnionFind{
private:
int* parent;
int* rank; //sz[i]表示以i为根的集合中元素的个数
int count;

public:
UnionFind(int count){
parent = new int[count];
this->count = count;
for(int i = 0; i < count; i++){
parent[i] = i;
rank[i] = 1;
}

}

~UnionFind(){
delete[] parent;
delete[] rank;
}

//找到从属的根节点
int find(int p){
assert(p >= 0 && p < count);
//直到p成为根节点
//            while(p != parent[p]){
//                parent[p] = parent[parent[p]];
//                p = parent[p];
//            }
//
//            return p;
//跳过一个进行搜索根,同时改变结构
if(p != parent[p])
parent[p] = find(parent[p]);

return parent[p];
}

bool isConnected(int p, int q){
return find(p) == find(q);
}

void unionElements(int p, int q){

int pRoot = find(p);
int qRoot = find(q);

if(pRoot == qRoot)
return;

if(rank[pRoot] < rank[qRoot]){
parent[pRoot] = qRoot;
}
else if(rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ //rank[qRoot] == rank[pRoot]
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}

}
};


后记

经过100000条随机数据测试,我们得到了如下性能数据:



当然,由于随机Union的情况不同,UF4稍微比UF3慢了一点,这里可以谅解。但是我们可以发现,理论上最快的UF5竟然明显地慢了。经过分析我们可以发现,因为是UF5使用了递归消耗了一定的时间导致变慢。所以说,算法的设计还得考虑实际的情况。我们需要考虑的方面还有很多。

图片引用百度图片

代码实现参照liuyubobobo慕课网教程
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: