【C++】【并查集】并查集实现、模板;并查集基于size、rank的优化;

一、引入并查集

○ 解决连接问题(网络中节点的连接问题)(两个社交网络中的人能不能根据好友互相认识)。
○ 数学中的集合类的实现。
○ 并查集的高效是因为只回答了连接问题,没有回答路径问题。

1、连接问题与路径问题:

连接问题要比路径问题回答的问题少,所以更高效。路径问题还是得看图论。

算法的实现:如果额外的回答了别的问题,那么可能存在更加高效的算法,只回答了需要的问题。

2、接口

并查集需要支持的接口:一组数据需要支持://优化到最后,其时间复杂度近乎是O(1);

Union(p,q)

find (p )

isconnected(p,q)

二、基础并查集

1、 Quickfind模式:

用ID 数组来表示两个节点的连接性。

Find 、isconnected只需要O(1) ;Union需要O(n),需要优化。

○ 初始化的时候,自己和自己是连通的。
○ 并的时候,需要O(n)让两个相连的连在一起。
○ 查的时候,只需要查ID数组中数一样,就是连接的。
在这里插入图片描述

1.1、实现

// Union-Find
class UnionFind {

private:
    int *id;    // 我们的第一版Union-Find本质就是一个数组
    int count;  // 数据个数

public:
    // 构造函数
    UnionFind(int n) {
        count = n;
        id = new int[n];
        // 初始化, 每一个id[i]指向自己, 没有合并的元素
        for (int i = 0; i < n; i++)
            id[i] = i;
    }

    // 析构函数
    ~UnionFind() {
        delete[] id;
    }

    // 查找过程, 查找元素p所对应的集合编号
    int find(int p) {
        assert(p >= 0 && p < count);
        return id[p];
    }

    // 查看元素p和元素q是否所属一个集合
    // O(1)复杂度
    bool isConnected(int p, int q) {
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
    // O(n) 复杂度
    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;
    }
};

2、Quickunion模式:

每个元素有一个指向父亲节点的指针。可以用数组来表示,数组中存的是指向父节点的指针。

○ 初始化的时候,指针指向自己,表示为自己为自己的根。
○ 并的时候,只需要让其根连到另一个的根上。
○ 查的时候,只需要查两个节点在不在一个根上。
在这里插入图片描述

2.1、实现

class UnionFind{

private:
    // 我们的第二版Union-Find, 使用一个数组构建一棵指向父节点的树
    // parent[i]表示第i个元素所指向的父节点
    int* parent;
    int count;  // 数据个数

public:
    // 构造函数
    UnionFind(int count){
        parent = new int[count];
        this->count = count;
        // 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
        for( int i = 0 ; i < count ; i ++ )
            parent[i] = i;
    }

    // 析构函数
    ~UnionFind(){
        delete[] parent;
    }

    // 查找过程, 查找元素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;

        parent[pRoot] = qRoot;
    }
};

3、Quickunion模式优化:

以下优化都是为了降低树高,使得降低操作时间复杂度。

3.1、 Quickunion优化size:

加一个size数组,表示当前根节点所在树的节点个数。

union 的时候让小size的并到大size的根中。降低树的高度。这样已经比上面两种快了一个数量级了。

union 的时候更新 size。
在这里插入图片描述

优化size实现

    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];
            }
        }
    };

3.2、 Quickunion优化rank:

size 优化存在的问题:因为size记录的不是树高,会造成矮胖树并到高瘦树中,造成树依然很高。

将size数组优化为rank数组,表示当前根节点的数高。

○ 并的时候,让矮的树并到高的树中;如果两个树一样高,将一个并到另一个,作为根的高度++;
○ 同样为了降低树的高度。但是对于size优化,效果不明显,因为极端的情况很少,反而因为判断增加,造成运算消耗。但是总体还是比size理想。

在这里插入图片描述

优化rank实现

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的值
        }
    }
};

3.3、路径压缩1 优化find

○ 优化find:让其向上找根的时候,如果父节点不是根,就让父节点指向父节点的父节点。

在这里插入图片描述

路径压缩1 实现

    class UnionFind{

    private:
        // rank[i]表示以i为根的集合所表示的树的层数
        // 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
        // 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
        // 关于这个问题,可以参考问答区:http://coding.imooc.com/learn/questiondetail/7287.html
        int* rank;
        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 );

            // path compression 1
            while( p != parent[p] ){
                parent[p] = parent[parent[p]];
                p = parent[p];
            }
            return p;

            // path compression 2, 递归算法
//            if( p != parent[p] )
//                parent[p] = find( parent[p] );
//            return parent[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的值
            }
        }
    };

3.4、路径压缩2 优化find

理论上最优,但是因为递归的消耗,使得效果可能没有上面的优化好。

优化find:让其向上找根的时候,递归的将其父节点返回,赋值给当前节点。使树的高度为2.
结束条件为,当前节点的父节点是自己,因为根是指向自己的。

在这里插入图片描述

路径压缩2 实现

class UnionFind{

public: // 后续, 我们要在外部操控并查集的数据, 在这里使用public
    int* parent; // parent[i]表示第i个元素所指向的父节点

private:
    // rank[i]表示以i为根的集合所表示的树的层数
    // 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
    // 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
    // 关于这个问题,可以参考问答区:http://coding.imooc.com/learn/questiondetail/7287.html
    int* rank;
    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 );

        // path compression 2, 递归算法
        if( p != parent[p] )
            parent[p] = find( parent[p] );
        return parent[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的值
        }
    }

    // 打印输出并查集中的parent数据
    void show(){
        for( int i = 0 ; i < count ; i ++ )
            cout << parent[i] << " ";
        cout <<endl;
    }
};

参考

bobo老师github

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值