并查集实现-(秩优化+路径压缩+java)

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。   
                                                    --百度百科

并查集(Union/Find)从名字可以看出,主要涉及两种基本操作:合并和查找。这说明,初始时并查集中的元素是不相交的,经过一系列的基本操作(Union),最终合并成一个大的集合。合并之后的结构逻辑上是一个森林。

应用

并查集数据结构非常简单,基本操作也很简单。用途也简单,就是检查两点是否联通。在实际中有很多应用,比如,求解无向图中连通分量的个数,生成迷宫……这些应用本质都是初始时都是一个个不连通的对象,经过一步步处理,变成连通的了,如迷宫,初始时,起点和终点不连通,随机地打开起点到终点路径上的一个方向,直至起点和终点连通了,就生成了一个迷宫。

存储结构

int count;// 元素数量
int[] rank; // 下标所对应节点层数
int[] parent;// 下标表示节点,值表示其父元素

主要函数及思路

  1. void Union(int p,int q);
    合并两个节点的思路就是,先找到两个节点的根节点,然后将一个节点接到另外一个节点下面即可,涉及到一个优化就是使用秩优化,因为如果随意将一个节点接到另外一个节点也是可以的,但是这种方法效率低下且慢,因为有时候一个根节点的层级很高,而另外一个层级很低,如果把层级高的放到层级低的下面这样层级就会增加,查找所用的时间就更长,数据少还好,多的时候效率太低了,所以引入一个数组记录每个根节点层数,每次合并把层数低的放到层数高的节点下面,这样层数就不会增加。
  2. int find(int p);
    此函数会返回p元素对应的根节点元素,因为我们初始化的时候根节点的父元素就是自己,所以我们查找的时候只要顺着查上去,遇到一个元素的父节点就是自己的,那么这个就是根节点了,在这里可以有一个优化,因为一旦元素一多起来,可能退化成一条链,每次得到根节点都将会使用O(n)的复杂度,这显然不是我们想要的。对此,我们必须要进行路径压缩,即我们找到最久远的祖先时“顺便”把它的子孙直接连接到它上面。这就是路径压缩了。

  3. boolean isConnected(int p, int q);
    只要调用find方法找到各自的根节点,如果p和q的根节点一样,那么就代表联通

具体代码:

import java.util.Random;

public class UnionFind {

    private int count;// 元素数量
    private int[] rank; // 层数
    private int[] parent;// 节点

    UnionFind(int count) { //初始化操作
        this.count = count;
        rank = new int[count];
        parent = new int[count];

        for (int i = 0; i < count; i++) {
            rank[i] = 1;
            parent[i] = i;

        }

    }

    public void Union(int p, int q) { // 合并两个元素

        int pRoot=find(p); //定位到其根节点
        int qRoot=find(q);
        if(pRoot==qRoot) //如果两个都相等,证明已经是连接好的,不必再合并
            return;

        if(rank[pRoot]>rank[qRoot]){ //如果p根节点的层数大于q的,那么将qRoot接到pRoot下面,这样接好以后层级不会增加
            parent[qRoot]=pRoot;

        }else if(rank[pRoot]<rank[qRoot]){

            parent[pRoot]=qRoot;
        }else //如果等于,则随便哪边层级都会加一
        {
            parent[pRoot]=qRoot;
            rank[qRoot]=rank[qRoot]+1;
        }


    }

    public int find(int p) {// 查找元素 返回根元素

        while(p!=parent[p]){
            parent[p]=parent[parent[p]]; //路径压缩
            p=parent[p];
        }

        return p;
    }

    public boolean isConnected(int p, int q) {

        return find(p) == find(q);
    }

    public static void main(String[] arg){

        int N=10000000; 
        UnionFind uf=new UnionFind(N);
        double startTime=System.currentTimeMillis();
        int tempA;
        int tempB;
        Random random = new Random();
        //进行N次合并操作
        for(int i=0;i<N;i++){
            tempA=random.nextInt(N)%N;
            tempB=random.nextInt(N)%N;
            uf.Union(tempA, tempB);

        }
        //进行N次查找
        for(int i=0;i<N;i++){
            tempA=random.nextInt(N)%N;
            tempB=random.nextInt(N)%N;
            uf.isConnected(tempA, tempB);

        }

        double endTime=System.currentTimeMillis();

        System.out.println(endTime-startTime);

    }


}

经过本机测试,千万级数据,只需要4025毫秒,相当于4秒,百万级数据217毫秒,0.217秒,当然这个数据根据电脑配置不同会有所不同,但是效率是很明显的,这就是并查集的优势所在,一个算法解决的问题越少所需要的时间应该越少。

猜你喜欢

转载自blog.csdn.net/hjsir/article/details/79294775
今日推荐