算法与数据结构之并查集

主要介绍并查集算法实现以及相关优化。c++

paste image

并查集 Union Find

  1. 图相关算法的实现。算法

  2. 一种不同的树形结构数组

链接问题 Connectivity Problem

可视化的来看链接问题:bash

链接问题

左上右下是不是链接的呢?微信

意义:实际应用中的做用

  • 网络中节点间的链接状态网络

    • 网络是个抽象的概念:用户之间造成的网络
  • 社交网络:Facebook中用户a和b中的联系(好友关系)。是否能联系到。函数

  • 音乐电影书籍,多媒体之间造成网络。性能

  • 互联网网页之间造成的网络测试

  • 路由器和路由器之间造成的也是网络优化

  • 道理交通,航班调度都是网络

数学中的集合类实现

并就是实现并集。& 查询

链接问题 & 路径问题

比路径问题要回答的问题少(路径是什么,链接问题只问有没有连)

  • 和二分查找做比较:顺序查找法顺便回答了rank。和前面其余元素的位置
  • 和select做比较:排好序回答问题更多。快排思路select回答问题更少
  • 和堆做比较:只关心最大最小。

除了回答问题自己以外是否是额外的回答了别的问题。颇有可能就存在
更高效的算法。:由于高效算法不须要回答额外的问题。

实现一个最简单的并查集 Union Find

对于一组数据,主要支持两个动做:

  • union( p , q )
  • find( p )

用来回答一个问题

  • isConnected( p , q )

最简单的表示方式;
数组。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)级别。可是并确很慢

并查集的另外一种实现思路

常规实现思路

将每个元素,看作是一个节点。

元素节点

每一个元素拥有一个指向父节点的指针。而后最上面的父节点指针指向本身。

Quick Union

数组存放父亲

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链接在一块儿:由于根相同。

成果

  • 其中6和2链接是6的根0和2的根1选取了1将0挂上。

代码实现

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更优了。

并查集的优化

问题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
复制代码

分析

  • 对于UF1来讲,虽然isConnected只须要O(1)的时间, 但因为union操做须要O(n)的时间;整体测试过程的算法复杂度是O(n^2)的
  • 对于UF2来讲, 其时间性能是O(n*h)的, h为并查集表达的树的最大高度
    • 这里严格来说, h和logn没有关系, 不过你们能够简单这么理解
    • 咱们后续内容会对h进行优化, 整体而言, 这个h是远小于n的
    • 因此咱们实现的UF2测试结果远远好于UF1, n越大越明显:)
  • 对于UF3来讲, 其时间性能依然是O(n*h)的, h为并查集表达的树的最大高度
    • 但因为UF3能更高几率的保证树的平衡, 因此性能更优

基于rank的并查集优化

分析

上面合并4和2 依靠集合的size来决定谁指向谁并不彻底合理。根据层数才最合理。

基于rank的优化

用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
复制代码

路径压缩(path Compression)

前面咱们都在优化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];
复制代码

最后的状况

  • 写一个递归的函数:调用findx,返回的就是x节点的根。让每一个parentx指向findx的结果。findx的结果也是Findparentx的结果。找x的时候,将x的Findparent的结果,指向父亲的结果。

优化状况并不明显。甚至由于递归的消耗。因此理论最优不必定实际好。

通过并查集的优化,并查集的操做,时间复杂度近乎是O(1)的


-------------------------华丽的分割线--------------------

看完的朋友能够点个喜欢/关注,您的支持是对我最大的鼓励。

我的博客番茄技术小栈掘金主页

想了解更多,欢迎关注个人微信公众号:番茄技术小栈

番茄技术小栈
相关文章
相关标签/搜索