算法学习——并查集
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慕课网教程
相关文章推荐
- 算法学习 并查集(Union-Find) (转)
- 算法模板学习专栏之并查集(一)入门
- 一个找亲戚游戏,引发了一场算法的学习——并查集
- 数据结构与算法学习-并查集
- 并查集。路径压缩 算法运用学习(一)
- 算法学习 并查集(笔试题目:找同伙)
- 算法学习基础篇(四):数据结构(堆、二叉搜索树、并查集)
- 算法:并查集学习笔记
- 【算法学习笔记】44. 并查集补充 SJTU OJ 3015 露子的星空
- 算法学习之并查集
- 【算法学习】【数据结构】并查集
- 算法分析学习笔记(一) - 动态连通性问题的并查集算法(上)
- 算法学习之并查集
- 并查集算法学习(转)
- poj 2524-小白算法学习 并查集 Ubiquitous Religions
- 算法学习——并查集
- 分享:C语言的学习基础,100个经典的算法
- 学习算法之路
- 算法入门学习----2.1归并排序
- 并查集(Union-Find)算法介绍