主要介绍并查集算法实现以及相关优化。c++
图相关算法的实现。算法
一种不同的树形结构数组
可视化的来看链接问题:bash
左上右下是不是链接的呢?微信
网络中节点间的链接状态网络
社交网络:Facebook中用户a和b中的联系(好友关系)。是否能联系到。函数
音乐电影书籍,多媒体之间造成网络。性能
互联网网页之间造成的网络测试
路由器和路由器之间造成的也是网络优化
道理交通,航班调度都是网络
数学中的集合类实现
并就是实现并集。& 查询
链接问题 & 路径问题
比路径问题要回答的问题少(路径是什么,链接问题只问有没有连)
除了回答问题自己以外是否是额外的回答了别的问题。颇有可能就存在
更高效的算法。:由于高效算法不须要回答额外的问题。
对于一组数据,主要支持两个动做:
用来回答一个问题
最简单的表示方式;
数组。0,1.
0-4 5-9
0-4是一组,5-9是一组。组内之间有联系,一组内的元素有相同的id
奇偶
奇数是一组,偶数是一组。
namespace UF1 {
class UnionFind {
private:
int *id;
int count;
public:
UnionFind(int n) {
count = n;
id = new int[n];
//初始条件每一个元素都是一组
for (int i = 0; i < n; i++)
id[i] = i;
}
~UnionFind() {
delete[] id;
}
//传入元素p,返回元素对应的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) {
//找到两个元素的id
int pID = find(p);
int qID = find(q);
//比较id
if (pID == qID)
return;
for (int i = 0; i < count; i++)
//从头至尾的扫描时间复杂度O(n)
if (id[i] == pID)
id[i] = qID;
}
};
}
复制代码
Testhelper.h:
namespace UnionFindTestHelper{
//n是数据量
void testUF1( int n ){
//
srand( time(NULL) );
UF1::UnionFind uf = UF1::UnionFind(n);
time_t startTime = clock();
//O(N*N)的时间复杂度
for( int i = 0 ; i < n ; i ++ ){
int a = rand()%n;
int b = rand()%n;
uf.unionElements(a,b);
//O(n)
}
for(int i = 0 ; i < n ; i ++ ){
int a = rand()%n;
int b = rand()%n;
uf.isConnected(a,b);
//时间复杂度只有O(1)
}
time_t endTime = clock();
cout<<"UF1, "<<2*n<<" ops, "<<double(endTime-startTime)/CLOCKS_PER_SEC<<" s"<<endl;
}
}
复制代码
main.cpp:
int main() {
int n = 100000;
UnionFindTestHelper::testUF1(n);
return 0;
}
复制代码
运行结果:
UF1, 200000 ops, 32.3533 s
[Finished in 39.7s]
复制代码
quick find 查找时只须要O(1)级别。可是并确很慢
常规实现思路
将每个元素,看作是一个节点。
元素节点
每一个元素拥有一个指向父节点的指针。而后最上面的父节点指针指向本身。
数组存放父亲
parent(i) = i;
union 3 4
union 3 8
union 6 5
union 9 4
要将9链接到4的根节点8上去。数组中:4-3-8-8 8是4的根节点。9指向8.
4和9链接在一块儿:由于根相同。
成果
namespace UF2{
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 );
while( p != parent[p] )
p = parent[p];
return p;
}
//看是否能找到一样的根
bool isConnected( int p , int q ){
return find(p) == find(q);
}
//找到p的根,和q的根
void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
//把根挂到另外一个的根
parent[pRoot] = qRoot;
}
};
}
复制代码
运行结果:
UF1, 20000 ops, 0.246341 s
UF2, 20000 ops, 0.059387 s
复制代码
当n大的时候,方法1更优了。
union 9,4 & union 4 9
union 9 4
9的元素少,将它指向4的根节点。造成的树层数低。
// 咱们的第三版Union-Find
namespace UF3{
class UnionFind{
private:
int* parent; // parent[i]表示第i个元素所指向的父节点
int* sz; // sz[i]表示以i为根的集合中元素个数
int count; // 数据个数
public:
// 构造函数
UnionFind(int count){
parent = new int[count];
sz = new int[count];
this->count = count;
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
sz[i] = 1;
}
}
// 析构函数
~UnionFind(){
delete[] parent;
delete[] sz;
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
int find(int p){
assert( p >= 0 && p < count );
// 不断去查询本身的父亲节点, 直到到达根节点
// 根节点的特色: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
bool isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
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];
}
}
};
}
复制代码
运行结果:
UF2, 200000 ops, 19.3316 s
UF3, 200000 ops, 0.0184 s
复制代码
上面合并4和2 依靠集合的size来决定谁指向谁并不彻底合理。根据层数才最合理。
用rank[i] 表示根节点为i的树的高度
namespace UF4{
class UnionFind{
private:
int* rank; // rank[i]表示以i为根的集合所表示的树的层数
int* parent; // parent[i]表示第i个元素所指向的父节点
int count; // 数据个数
public:
// 构造函数
UnionFind(int count){
parent = new int[count];
rank = new int[count];
this->count = count;
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 析构函数
~UnionFind(){
delete[] parent;
delete[] rank;
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
int find(int p){
assert( p >= 0 && p < count );
// 不断去查询本身的父亲节点, 直到到达根节点
// 根节点的特色: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
bool isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
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[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
};
}
复制代码
对于UF3来讲, 其时间性能依然是O(n*h)的, h为并查集表达的树的最大高度,但因为UF3能更高几率的保证树的平衡, 因此性能更优
UF4虽然相对UF3进行有了优化, 但优化的地方出现的状况较少,因此性能更优表现的不明显, 甚至在一些数据下性能会更差,由于判断更多了。
运行结果
2000000 ops, 0.313945 s
复制代码
前面咱们都在优化union。其实Find咱们也能够进行优化。因为每一个节点存的都是它的父亲节点,全部每一个节点均可以有无数个(多个)孩子。在search值的时候,对于没有找到的根的节点,能够往上挪一挪。
好比咱们要find4
咱们将4的父亲节点链接为4的父亲的父亲(若是出现3就是根节点,也没有关系,由于对于根节点来讲,3的父亲仍是3)
下面考虑4的parent:2 (此时跳过了3,跳2级是没有问题的)
最后的结果:
修改 find函数
int find(int p){
assert( p >= 0 && p < count );
// path compression 1
while( p != parent[p] ){
parent[p] = parent[parent[p]];
p = parent[p];
}
}
复制代码
//path compression 2, 递归算法
if( p != parent[p] )
parent[p] = find( parent[p] );
return parent[p];
复制代码
最后的状况
优化状况并不明显。甚至由于递归的消耗。因此理论最优不必定实际好。
通过并查集的优化,并查集的操做,时间复杂度近乎是O(1)的
-------------------------华丽的分割线--------------------
看完的朋友能够点个喜欢/关注,您的支持是对我最大的鼓励。
想了解更多,欢迎关注个人微信公众号:番茄技术小栈