赣州生活网:并查集(Union Find)

admin 6个月前 (05-03) 科技 38 0

目录
  • 并查集先容
  • 第一种实现
  • 第二种实现
  • 并查集的优化方式
    • 思量size
    • 基于rank的优化
    • 路径压缩优化

并查集先容

  我们之前讲的树结构,都是由父亲节点指向孩子节点,而并查集却是由孩子指向父亲的这样一种数据结构。

  给出图中随便的两点,问这两点之间是否可以通过一个路径毗邻起来?并查集就是处置这类毗邻问题的很好的数据结构。即用来处置网络中节点的毗邻状态,这里的网络是个抽象慨念,可以是用户之间形成 的网络。实在这类毗邻问题我们也可以使用聚集类来举行实现,即求两个聚集的并集。

本文设计的并查集主支持两个操作:

  1. union(p,q) 并,对传入的两个数据p和q,在并查集内部将这两个数据,以及这两个数据所在的聚集合并起来。
  2. isConnected(p,q)查询对于给定的两个数据,他们是否属于同一个聚集。

由于并查集可以有差别的实现,我们可以设计一个并查集的接口:

public interface UF {

    int getSize();
    boolean isConnected(int p ,int q);
    void unionElements(int p,int q);

}

  对于详细元素是谁,我们并不体贴,我们体贴的是 id=p 和 id=q 这样的两个元素是否相连,而对于p和q对应详细的值,并不体贴。

第一种实现

  示意每一个元素都属于差别的聚集,没有随便两个元素是相连的

public class UnionFind01 implements UF {

    //我们第一种实现,Union-Find本质就是一个数组
    private int[] id;

    //有参组织:指定并查集中元素的个数
    public UnionFind01(int size){
        id = new int[size];
        //初始化, 每一个id[i]指向自己, 没有合并的元素
        for (int i=0; i<id.length; i++)
            id[i] = i;
    }

    @Override
    public int getSize() {
        return id.length;
    }

    // 查找元素p所对应的聚集编号
    // O(1)复杂度
    private int find(int p){
        if (p < 0 || p >= id.length)
            throw  new IllegalArgumentException("p is out of bound");
        return id[p];
    }

    // 查看元素p和元素q是否所属一个聚集
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的聚集
    // O(n) 复杂度
    @Override
    public void unionElements(int p, int q) {
        int pID = find(p);
        int qID = find(q);

        if (pID == qID)
            return;

        // 合并历程需要遍历一遍所有元素, 将两个元素的所属聚集编号合并
        for (int i = 0; i < id.length; i++){
            if (id[i] == pID)
                id[i] = qID;
        }
    }
}

第二种实现

  我们的第二版Union-Find, 使用一个数组构建一棵指向父节点的树,如图:

  我们在初始化时,有10个元素,编号分别为0至9,即可以看成一棵只含有一个根节点的树结构,这个根节点的值即为索引值0至9,毗邻操作图如下:
赣州生活网:并查集(Union Find) 第1张
代码实现如下:

public class UnionFind02 implements UF {

    //parent[i]示意第i个元素所指向的父节点
    private int[] parent;

    //组织函数
    public UnionFind02(int size) {
        parent = new int[size];

        // 初始化, 每一个parent[i]指向自己, 示意每一个元素自己自成一个聚集
        for (int i = 0; i < size; i++){
            parent[i] = i;
        }
    }

    @Override
    public int getSize() {
        return parent.length;
    }

    // 查找历程, 查找元素p所对应的聚集编号
    // O(h)复杂度, h为树的高度
    public int find(int p){
        if (p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p id out of bound");

        // 不断去查询自己的父亲节点, 直到到达根节点
        // 根节点的特点: parent[p] == p
        while (p != parent[p])
            p = parent[p];
        return p;
    }

    // 查看元素p和元素q是否所属一个聚集
    // O(h)复杂度, h为树的高度
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的聚集
    // O(h)复杂度, h为树的高度
    @Override
    public void unionElements(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);

        if (pRoot == qRoot)
            return;

        parent[pRoot] = qRoot;
    }
}

  现在让我们来简朴测试下并查集的这两种实现方式的效率:

public class Test {

    //简朴测试两种并查集实现方式的效率
    private static double testUF(UF uf, int m){
        int size = uf.getSize();
        Random random = new Random();

        long startTime = System.nanoTime();
        //测试毗邻操作
        for (int i = 0; i < m ; i ++){
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.unionElements(a,b);
        }

        //测试这两个元素是否毗邻
        for (int i = 0; i < m ; i ++){
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.isConnected(a,b);
        }

        long endTime = System.nanoTime();

        return (endTime - startTime) / 1000000000.0;
    }

    public static void main(String[] args) {
        int size = 100000;
        int m = 10000;
        //第一种实现
        UnionFind01 uf01 = new UnionFind01(size);
        System.out.println("UnionFind01 -> "+ testUF(uf01,m) +"s");
        //第二种实现
        UnionFind02 uf02 = new UnionFind02(size);
        System.out.println("UnionFind02 -> "+ testUF(uf02,m) +"s");

    }

}

测试代码运行效果:
赣州生活网:并查集(Union Find) 第2张
通过测试代码的运行效果可以看出,我们的第二种实现方式是要比第一种实现方式 效率高一些的,现在我们看一个关于并查集的应用,就是力扣网上的547号练习题,问题形貌如下:
赣州生活网:并查集(Union Find) 第3张
现在就让我们基于并查集的第二种实现方式,来解决这个问题:

    public int finDCircleNum(int[][] M) {

        int n = M.length;
        UnionFind2 uf = new UnionFind2(n);
        for(int i = 0 ; i < n ; i ++)
            for(int j = 0 ; j < i ; j ++)
                //若是M[i][j] = 1,则示意第 i 个和 j 个学生互为同伙,即属于同一个聚集
                if(M[i][j] == 1)
                    uf.unionElements(i, j);

        TreeSet<Integer> set = new TreeSet<>();
        for(int i = 0 ; i < n ; i ++)
            set.add(uf.find(i));
        //获取差别聚集的个数
        return set.size();
    }

并查集的优化方式

思量size

  由于我们之前的实现方式,在举行毗邻操作时,总是将左边的元素指向右边的元素,这样很容易就会形成一个链表,这样的查询时间复杂度就会酿成O(n),为了制止这样极端的情形,我们可以在每个节点中添加一个元素个数的变量,即该节点下毗邻的节点个数,这样在举行毗邻操作时,就将节点加到节点个数少的聚集中去,如图:
赣州生活网:并查集(Union Find) 第4张
赣州生活网:并查集(Union Find) 第5张
代码实现如下:

public class UnionFind03 implements UF {

    private int[] parent; // parent[i]示意第i个元素所指向的父节点
    private int[] sz;     // sz[i]示意以i为根的聚集中元素个数

    public UnionFind03(int size){
        parent = new int[size];
        sz = new int[size];

        // 初始化, 每一个parent[i]指向自己, 示意每一个元素自己自成一个聚集
        for(int i = 0 ; i < size ; i ++){
            parent[i] = i;
            sz[i] = 1;
        }
    }

    @Override
    public int getSize() {
        return parent.length;
    }

    // 查找历程, 查找元素p所对应的聚集编号
    // O(h)复杂度, h为树的高度
    public int find(int p){
        if (p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // 不断去查询自己的父亲节点, 直到到达根节点
        // 根节点的特点: parent[p] == p
        while ( p != parent[p])
            p = parent[p];
        return p;
    }

    // 查看元素p和元素q是否所属一个聚集
    // O(h)复杂度, h为树的高度
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的聚集
    // O(h)复杂度, h为树的高度
    @Override
    public 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 {// sz[qRoot] <= sz[pRoot]
            parent[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
    }
}

我们可以通过测试方式,对这三种实现方式举行简朴的对照,我们会发现,优化size之后的并查集,效率比前两种是要好许多的。
赣州生活网:并查集(Union Find) 第6张

基于rank的优化

  rank[i]示意根节点为i的树的高度,即凭据两个元素所在的输的rank差别判断合并偏向,将rank低的合并到rank高的聚集上
赣州生活网:并查集(Union Find) 第7张
代码实现如下:

public class UnionFind04 implements UF {
    private int[] rank;   // rank[i]示意以i为根的聚集所示意的树的层数
    private int[] parent; // parent[i]示意第i个元素所指向的父节点

    // 组织函数
    public UnionFind04(int size){

        rank = new int[size];
        parent = new int[size];

        // 初始化, 每一个parent[i]指向自己, 示意每一个元素自己自成一个聚集
        for( int i = 0 ; i < size ; i ++ ){
            parent[i] = i;
            rank[i] = 1;
        }
    }

    @Override
    public int getSize(){
        return parent.length;
    }

    // 查找历程, 查找元素p所对应的聚集编号
    // O(h)复杂度, h为树的高度
    private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // 不断去查询自己的父亲节点, 直到到达根节点
        // 根节点的特点: parent[p] == p
        while(p != parent[p])
            p = parent[p];
        return p;
    }

    // 查看元素p和元素q是否所属一个聚集
    // O(h)复杂度, h为树的高度
    @Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的聚集
    // O(h)复杂度, h为树的高度
    @Override
    public void unionElements(int p, int q){

        int pRoot = find(p);
        int qRoot = find(q);

        if( pRoot == qRoot )
            return;

        // 凭据两个元素所在树的rank差别判断合并偏向
        // 将rank低的聚集合并到rank高的聚集上
        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的值
        }
    }
}

现在让我们对基于size的优化,和基于rank的优化举行简朴的测试对比:
赣州生活网:并查集(Union Find) 第8张

路径压缩优化

  为什么需要路径压缩优化呢?基于前面的四种实现方式,我们会发现下图中的三颗并查集数,无论是find()照样isConnected()都是等效的
赣州生活网:并查集(Union Find) 第9张
  由于并查集的查找方式是和树得高度相关的,以是我们只要让树得高度降低,就都是对并查集的优化,理想情形下,我们树得高度为2,即只有根节点和叶子节点。但实际情形并不都是云云,以是我们只能尽可能降低树得高度,这就是我们的路径压缩优化。那我们的路径压缩是若何实现的呢?我们在查找某个节点的时刻,让这个节点指向它父亲节点的父亲节点,然后判断这个节点是否是待查找节点的根节点,如不是,再使用待查找节点指向它父亲节点的父亲节点,如下图:
赣州生活网:并查集(Union Find) 第10张
赣州生活网:并查集(Union Find) 第11张
  凭据图中剖析,可以看出我们的路径压缩是在查找历程中实现的,以是我们修改查找方式即可:代码 实现如下:

    // 查找历程, 查找元素p所对应的聚集编号
    // O(h)复杂度, h为树的高度
    private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        while( p != parent[p] ){
            parent[p] = parent[parent[p]];
            p = parent[p];
        }
        return p;
    }
,

suNBet

www.43zhekou.com在即将到来的2019年,将以更暖心的服务,更完善的技术,更足够的资金,为所有Sunbet的代理、会员提供更好的开户、买分服务。

皇冠体育声明:该文看法仅代表作者自己,与本平台无关。转载请注明:赣州生活网:并查集(Union Find)

文章归档

站点信息

  • 文章总数:653
  • 页面总数:0
  • 分类总数:8
  • 标签总数:1192
  • 评论总数:233
  • 浏览总数:19902