数据结构(九) ---[实现并查集(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 findParent(int index){
        while(index !=this.parent[index]){
            //将当前操作节点的父节点的父节点 与 当前的节点挂接;
            parent[index] = parent[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;
    }
}

📢实现方式6: 优化

可以再优化优化,降低树的高度;

在这里插入图片描述

这里具体实现的话,就需要在查找节点索引时进行优化;层层递归,

在这里插入图片描述

import com.company.unionset.UnionSet;
/**
 * @author by CSDN@小智RE0
 * @date 2021-12-09
 * 方式6::最终优化
 */
public class RealUnionSet06 implements UnionSet {
    //使用数组作为底层结构;就是存入父级节点的元素;
    int[] parent;

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

    //辅助的方法;findParent;  优化点----->
    private int findParent(int index){
        if (index !=this.parent[index]){
            //递归找父级节点;
            parent[index] = findParent(parent[index]);
        }
        return parent[index];
    }



    @Override
    public void toUnion(int p, int q) {
        //分别找到这两个索引的对应顶级元素;
        int pEle = findParent(p);
        int qEle = findParent(q);
        //若没有连接,就把两个索引下的元素置为同一个;
        if (pEle != qEle) {
            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;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小智RE0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值