数据结构 ---[实现并查集(UnionFind)]

Posted 小智RE0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构 ---[实现并查集(UnionFind)]相关的知识,希望对你有一定的参考价值。

刚开始学并查集之前,也是提前找了一点资料看看,
推荐看这位博主的文章;讲解并查集通俗易懂. 知乎博主:Pecco -->算法学习笔记(1) : 并查集


可用并查集解决网络上的节点连接问题;具体说的话右两个功能;(1)判断两个点是否连接,(2)若没有连接就进行连接

首先在实现之前定义一个通用的接口UnionSet;

在使用并查集时,并不关心元素内容,
因此可以使用数组保存元素,p和q分别对应元素所在数组的索引

/**
 * @author by CSDN@小智RE0
 * @date 2021-12-08 
 * 并查集通用接口
 */
public interface UnionSet 
    /**
     * 定义的连接方法
     * @param p 第一个集合的元素当前索引
     * @param q 第二个集合的元素当前索引
     */
    void toUnion(int p, int q);

    /**
     * 判断两个集合是否连接
     * @param p 集合1的元素的当前索引
     * @param q 集合2的元素的当前索引
     * @return  是否连接
     */
    boolean isConnection(int p, int q);

    /**
     * 返回当前集合的元素个数;
     * @return 元素的个数
     */
    int getSize();

📢实现方式1:使用数组基础实现

过程

比如这样,可以把数组中的每个元素都看做是单独的集合;

例如要查看节点1 和 4 是否连接; 查到节点1的数组元素内容是1,节点4的数组元素内容是0;不相等,所以没有连接;

那么,要对节点1和节点4进行连接,实际上就得把他们所属的集合合并起来;
将节点4所属集合的所有集合元素都替换为1即可;

代码实现

import com.company.unionset.UnionSet;

/**
 * @author by CSDN@小智RE0
 * @date 2021-12-08
 * 方式1:::数组基础实现并查集;
 */
public class RealUnionSet01 implements UnionSet 
    //存储元素的数组;
    int[] data;

    //初始化;
    public RealUnionSet01(int len)
        this.data = new int[len];
        //向数组存入元素;
        for (int i = 0; i < len; i++) 
            this.data[i] = i;
        
    

    //用于查找当前索引的对应元素;
    public int findEle(int index)
        return this.data[index];
    

    @Override
    public void toUnion(int p, int q) 
        //找到这两个索引的对应元素;
        int pEle = findEle(p);
        int qEle = findEle(q);
        if(pEle!=qEle) 
            //第一个集合的元素统一重置为第二个集合的;
            for (int i = 0; i < this.data.length; i++) 
                if (findEle(i) == pEle) 
                    this.data[i] = qEle;
                
            
        
    


    @Override
    public boolean isConnection(int p, int q) 
        //分别找到这两个索引的对应元素;
        int pEle = findEle(p);
        int qEle = findEle(q);
        return pEle == qEle;
    

    @Override
    public int getSize() 
        return data.length;
    

测试1

/**
 * @author by CSDN@小智RE0
 * @date 2021-12-08 
 * 测试方式1实现的
 */
public class Test01 
    public static void main(String[] args) 
        int len = 100000;
        int num = 100000;
        RealUnionSet01 rs1 = new RealUnionSet01(len);
        test1(rs1,num);
    

    /**
     * 在并查集下测试num次连接
     * @param rs  并查集
     * @param num 次数
     */
    public static void test1(UnionSet rs, int num)
        long startTime = System.nanoTime();
        //使用随机数类;
        Random rd = new Random();
        for (int i = 0; i < num; i++) 
            int n1 = rd.nextInt(rs.getSize());
            int n2 = rd.nextInt(rs.getSize());
            rs.toUnion(n1,n2);
        
        long endTime = System.nanoTime();
        System.out.println("连接用时"+(endTime-startTime)/1000000000.0);
    

连接用时6.4901904

📢实现方式2:以数组为基础,树形结构实现

过程

刚开始每个节点都是自连接的;

若要将节点2和节点3进行连接,节点1还是自连接

然后将节点1与节点3连接,只要将节点1和节点3的根节点连接即可;

只要节点所在的那棵树的顶层节点相同,就认为这两棵树是连接的;
顶层节点的特点:::>>>它的父节点就是自己,

例如用数组作为底层结构,刚开始都是指向自己;每个节点的顶层节点都是自己;进行自连接;

用树结构来看的话,就是下面这样;

比如说要将节点3和节点4进行连接;将节点3作为节点4的顶级节点即可;

那么,要查看节点是否连接,看看该节点索引的元素是否相等即可;

节点3和节点8进行连接;节点3的父节点改为节点8即可

节点6和节点5进行连接;节点6的父节点改为节点5即可;

节点9和节点4进行连接;节点9指向节点4的顶层节点;

节点2和节点1连接,

节点5和节点0连接;

节点7和节点2进行连接;节点7连到节点2的父节点1上;

节点6和节点2进行连接;将节点6的顶层节点0拼接到节点2的顶层节点1上

代码实现

import com.company.unionset.UnionSet;
/**
 * @author by CSDN@小智RE0
 * @date 2021-12-08 
 * 方式2::
 */
public class RealUnionSet02 implements UnionSet 

    //使用数组作为底层结构;就是存入父级节点的元素;
    int[] parent;

    //初始化;
    public RealUnionSet02(int len)
        this.parent = new int[len];
        //初始化都是自连接;
        for (int i = 0; i < len; i++) 
            this.parent[i] = i;
        
    

    //辅助的方法;findParent;
    private int findParent(int index)
        while(index !=this.parent[index])
            index = this.parent[index];
        
        return index;
    

    @Override
    public void toUnion(int p, int q) 
        //分别找到这两个索引的对应顶级元素;
        int pEle = findParent(p);
        int qEle = findParent(q);
        //若没有连接,就把两个索引下的元素置为同一个;
        if(pEle!=qEle)
           this.parent[pEle] = qEle;
        
    

    @Override
    public boolean isConnection(int p, int q) 
        //分别找到这两个索引的对应顶级元素;
        int pEle = findParent(p);
        int qEle = findParent(q);
        return pEle == qEle;
    

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

测试2

public class Test02 
    public static void main(String[] args) 
        int len = 100000;
        int num = 100000;
        RealUnionSet02 rs2 = new RealUnionSet02(len);
        test2(rs2,num);
    

    /**
     * 在并查集下测试num次连接
     * @param rs  并查集
     * @param num 次数
     */
    public static void test2(UnionSet rs, int num)
        long startTime = System.nanoTime();
        //使用随机数类;
        Random rd = new Random();
        for (int i = 0; i < num; i++) 
            int n1 = rd.nextInt(rs.getSize());
            int n2 = rd.nextInt(rs.getSize());
            rs.toUnion(n1,n2);
        
        long endTime = System.nanoTime();
        System.out.println("连接用时"+(endTime-startTime)/1000000000.0);
    

连接用时3.321709

📢实现方式3:(优化方式2),添加一个辅助数组存储每棵树的节点个数

不管是查询操作还是合并操作,使用树结构形式的
并查集的复杂度为O(h),在极端的情况下,这棵树的深度会比较大。

比如在连接节点0和3时,这样直接挂接会增大树的深度;降低效率;

具体优化树的连接操作;将小树连接到大树上面;
将元素个数少的树合并到元素个数多的树上。需要开辟空间,存放每棵树的元素个数。

那么就得需要单独在创建出一个数组用来存储树的节点个数;

import com.company.unionset.UnionSet;
/**
 * @author by CSDN@小智RE0
 * @date 2021-12-09
 * 方式3::(对实现方式2的优化)
 */
public class RealUnionSet03 implements UnionSet 

    //使用数组作为底层结构;就是存入父级节点的元素;
    int[] parent;
    //存储每棵树的节点个数;
    int[] size;

    //初始化;
    public RealUnionSet03(int len)
        this.parent = new int[len];
        this.size   = new int[len];
        //初始化都是自连接;
        for (int i = 0; i < len; i++) 
            this.parent[i] = i;
            //初始化,每棵树都是单独自连接;
            this.size[i] = 1;
        
    

    //辅助的方法;findParent;
    private int findParent(int index)
        while(index !=this.parent[index])
            index = this.parent[index];
        
        return index;
    

    //优化点;---->
    @Override
    public void toUnion(int p, int q) 
        //分别找到这两个索引的对应顶级元素;
        int pEle = findParent(p);
        int qEle = findParent(q);
        //若没有连接,就把两个索引下的元素置为同一个;
        if(pEle!=qEle)
          //需要比较树的节点个数;把小树接到大树上;
            if(size[pEle] >size[qEle])
                parent[qEle] = pEle;
                size[pEle]   += size[qEle]; 
            else
                parent[pEle] = qEle;
                size[qEle]   += size[pEle];
            
        
    

    @Override
    public boolean isConnection(int p, int q) 
        //分别找到这两个索引的对应顶级元素;
        int pEle = findParent(p);
        int qEle = findParent(q);
        return pEle == qEle;
    

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

📢实现方式4:(优化方式3) 按秩合并

方式三虽然进行小树连接到大树的操作,但是还有一个问题,可能会出现下面这种情况的树;

比如,经过一系列连接后,

现在要连接节点4和节点2;由于节点2所在的树结点比较多,就得让节点4所在的树合并到节点2所在的树;就出现这样情况了;也是会影响效率;

import com.company.unionset.UnionSet;
/**
 * @author by CSDN@小智RE0
 * @date 2021-12-09
 * 方式4::(对实现方式3的优化) --> 对树的高度进行优化;
 * 按秩合并;
 */
public class RealUnionSet04 implements UnionSet 

    //使用数组作为底层结构;就是存入父级节点的元素;
    int[] parent;
    //存储每棵树的高度;
    int[] rank;

    //初始化;
    public RealUnionSet04(int len)
        this.parent = new int[len];
        this.rank   = new int[len];
        //初始化都是自连接;
        for (int i = 0; i < len; i++) 
            this.parent[i] = i;
            //初始化,每棵树都是单独自连接;
            this.rank[i] = 1;
        
    

    //辅助的方法;findParent;
    private int findParent(int index)
        while(index !=this.parent[index])
            index = this.parent[index];
        
        return index;
    

    //优化点;---->
    @Override
    public void toUnion(int p, int q) 
        //分别找到这两个索引的对应顶级元素;
        int pEle = findParent(p);
        int qEle = findParent(q);
        //若没有连接,就把两个索引下的元素置为同一个;
        if (pEle != qEle) 
            //由低的树到高的树;
            if (rank[pEle] > rank[qEle]) 
                parent[qEle] = pEle; //注意高度不变;
             else if (rank[pEle] < rank[qEle]) 
                parent[pEle] = qEle;
             else 
                //若相等;合并后,高度要变化;
                parent[qEle] = pEle;
                rank[pEle] += 1;
            
        
    

    @Override
    public boolean isConnection(int p, int q) 
        //分别找到这两个索引的对应顶级元素;
        int pEle = findParent(p);
        int qEle = findParent(q);
        return pEle == qEle;
    

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

📢实现方式5:路径压缩

具体的过程,可以这么看,让节点4作为当前操作节点,把它挂接到父级节点3的父级节点2上;

然后再让节点2作为当前操作节点;让它挂接到父级节点1的父级节点0上;

import com.company.unionset.UnionSet;
/**
 * @author by CSDN@小智RE0
 * @date 2021-12-09
 * 方式5::路径压缩法
 */
public class RealUnionSet05 implements UnionSet 
    //使用数组作为底层结构;就是存入父级节点的元素;
    int[] parent;
    //存储每棵树的高度;
    int[] rank;

    //初始化;
    public RealUnionSet05(int len)
        this.parent = new int[len];
        this.rank   = new int[len];
        //初始化都是自连接;
        for (int i = 0; i < len; i++) 
            this.parent[i] = i;
            //初始化,每棵树都是单独自连接;
            this.rank[i] = 1;
        
    

    //辅助的方法;findParent;  优化点----->
    private int findParentjava——并查集 UnionFind

并查集(UnionFind)

并查集(UnionFind)

并查集

并查集

数据结构学习 -- 并查集