并查集学习总结

并查集是一种很不一样的数据结构。可以解决连接问题,路径问题,主要有两个操作:

  1. union(p, q)//将p和q所在的集合合并在一起
  2. find(p) //找到p属于哪个集合

可以非常容易的实现判断p和q是否相连,是否在一个集合。

1. 并查集实现思路1(quick find)

由quick find可以看出,此种实现思路会在执行find操作是具有较高的效率。

其实思路很简单,假设建立了一个大小为n的并查集,此时说明并查集中有n个集合。设置一个id数组,初始化时id[i]=i。

由此执行find(p)操作时,可以直接返回id[p]即可,可以看出时间复杂度在O(1)即可完成操作

但是在执行union(p, q)是,需要遍历整个id数组,将所有p的id值等于q的id值(反过来也行)达成合并的操作,时间复杂度为O(n)

#include <iostream>
#include <cassert>
namespace UF1 {
    class UnionFind {

    private:
        int count;
        int *id;

    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(pId == id[i])
                    id[i] = qId;
        }
    };
}

2. 并查集实现思路2(quick union)

相比于第一种思路,这一种思路才是最常规的并查集实现思路。

使用一个parent数组,parent[i]存储的是节点i的父节点。

parent[i] = i, 代表父指针指向自己,该节点为当前集合的根节点。如何判断p和q是否属于一个集合,只需要分别找到p和q的根节点即可,如果是一个根节点,说明p和q在一个集合,否则不再一个根节点。

find(p)操作

从p向上不断的找父节点,直到找到根节点即可:

int find(int p) {
        assert(p >= 0 && p < count);
        while(p != parent[p])
            p = parent[p];
        return p;
    }
union(p, q)操作

找到p和q的根节点,然后将q(p)的父指针指向p(q)即可。

void unionElements(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if(pRoot == qRoot)
            return;
        parent[pRoot] = qRoot;
    }

完整实现如下:

class UnionFind {

private:
    int *parent;
    int count;

public:
    UnionFind(int n) {
        parent = new int[n];
        this->count = n;
        for(int i = 0; i < n; 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(p);
    }

    void unionElements(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if(pRoot == qRoot)
            return;
        parent[pRoot] = qRoot;
    }
};

3. 并查集的优化一(size)

由前面的实现来看,在执行union(p, q)的时候,因为我们是将p或q任意执行另一集合的根节点,这样非常容易导致一个问题------有可能会导致一棵树的高度过高,在执行find操作效率变低。

我们应该尽量去平衡每一个集合,通过size数组来实现, sz[i]来表示以i为根的集合的元素个数,这样在执行union操作时可以先比较两个集合的size,将size小的集合根节点指向size大的集合的根节点。

class UnionFind {

    private:
        int *parent;
        int *sz;//sz[i]表示以i为根的集合中元素的个数
        int count;

    public:
        UnionFind(int n) {
            parent = new int[n];
            sz = new int[n];
            this->count = n;
            for(int i = 0; i < n; i++) {
                parent[i] = i;
                sz[i] = 1;
            }
        }

        ~UnionFind() {
            delete [] parent;
            delete [] sz;
        }

        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(p);
        }

        //此方法使得一个集合的树更矮一些
        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. 并查集的优化二(rank)

在有些情况你可能会发现用size来优化效果并不好,比如,有些集合的树形结构并不高,但是节点多(size大);有些树形结构高,但是节点少(size小),然后将size小的指向了size大的集合,即将树高的集合根节点指向了矮树的根节点,实际上增加了树的高度,在执行find的时候,当查找元素p处在很高的那个分支,需要执行非常多的递归操作,所以可以通过评判两颗树的深度(高度)来选择合并时的指向。

如果rank[p] > rank[q], 那么将q集合根节点执行p节点根节点。

class UnionFind {

    private:
        int *parent;
        int *rank;//rank[i]表示以i为根的树的高度
        int count;

    public:
        UnionFind(int n) {
            parent = new int[n];
            rank = new int[n];
            this->count = n;
            for(int i = 0; i < n; i++) {
                parent[i] = i;
                rank[i] = 1;
            }
        }

        ~UnionFind() {
            delete [] parent;
            delete [] rank;
        }

        int find(int p) {
            assert(p >= 0 && p < count);
            while(p != parent[p]){
                parent[p] = parent[parent[p]];//路径压缩
                p = parent[p];

            }
            return p;
        }

        bool isConnected(int p, int q) {
            return find(p) == find(p);
        }

        //此方法使得一个集合的树更矮一些
        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[pRoot] > rank[qRoot]){
                parent[qRoot] = pRoot;
            }
            else {
                parent[pRoot] = qRoot;
                rank[qRoot]++;
            }
        }

    };

4. 并查集的优化三(路径压缩)

我们可以尽量使find的执行此时少,即所在集合形成的树个深度越小越好,最为理想的情况是,所有节点的值直接执行根节点。

通过每次find操作,实现路径压缩,将p指向根节点

int find(int p) {
   assert(p >= 0 && p < count);
   if(p != parent[p])
       parent[p] = find(parent[p]);
   return parent[p];
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值