算法学习——并查集

前言

大家肯定有听说过社交网络里面的六人理论吧,说是可以通过六个人的联系认识世界上的任意一个人。比如我想认识一下机械系的系花,我先找到机械系的朋友,然后通过朋友介绍认识。这样可以发现我们的社交圈子其实是有部分重叠的。当然也有可能是我的圈子太小,根本没有什么朋友认识那个圈子里面的人,也有很大可能是机械系花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[n];
            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处即可,这样通过查找根节点是否相同,我们就可以判断两者是否连接。
优化1
代码如下:
我们不断查找父节点的父节点,直到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图示:
sz
rank图示:
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慕课网教程

猜你喜欢

转载自blog.csdn.net/blueblueskyz/article/details/79444101