并查集详解:UF——UF_Tree——UF_Tree_Weighted逐步优化

1 并查集简介

并查集是一种树型的数据结构,它可以高效地进行如下操作:

  1. 查询元素p和元素q是否属于同一组;
  2. 合并元素p和元素q所在的组。

在这里插入图片描述
特点

  1. 每个元素都唯一对应一个结点;
  2. 每一组数据中的多个元素都在同一个树中;
  3. 一组中的数据对应的树和另外一组中的数据对应的树之间没有任何联系;
  4. 元素在树中没有子父级关系的硬性要求。

在这里插入图片描述

2 UF

2.1 UF(int N)构造方法实现

  1. 初始情况下,每个元素都在一个独立的分组中,因此,初始情况下并查集中的数据分为N个组;
  2. 初始化数组eleAndGroup,索引是每个结点存储的元素,索引对应的值是该结点所在的分组,初始情况下,i索引处的值就是i。

在这里插入图片描述

2.2 union(int p, int q)合并方法实现

  1. 如果p和q已经在同一个分组中,无需合并;
  2. 如果p和q不在同一个分组,将p所在分组中所有元素的组标识符修改为q所在组的标识符,同时分组数量-1。

在这里插入图片描述

2.3 代码实现(Java)

public class UF {
    //记录结点元素和该元素所在分组的标识
    private int[] eleAndGroup;
    //记录并查集中数据的分组个数
    private int count;

    //初始化并查集
    public UF(int N){
        //初始化分组的数量,默认情况下,有N个分组
        this.count = N;
        //初始化eleAndGroup数组
        this.eleAndGroup = new int[N];
        //初始化eleAndGroup中的元素及其所在的组的标识符,让eleAndGroup数组的索引作为并查集的每个结点的元素,并且让每个索引处的值(该元素所在的组的标识符)等于该索引
        for (int i = 0; i < eleAndGroup.length; i++) {
            eleAndGroup[i] = i;
        }
    }

    //获取当前并查集中的数据有多少个分组
    public int count(){
        return count;
    }

    //元素p所在分组的标识符
    public int find(int p){
        return eleAndGroup[p];
    }

    //判断并查集中元素p和元素q是否在同一分组中
    public boolean connected(int p,int q){
        return find(p) == find(q);
    }

    //把p元素所在分组和q元素所在分组合并
    public void union(int p,int q){
        //判断元素q和p是否已经在同一分组中,如果已经在同一分组中,结束方法
        if (connected(p,q)){
            return;
        }
        //找到p所在分组的标识符
        int pGroup = find(p);
        //找到q所在分组的标识符
        int qGroup = find(q);
        //合并分组:让p所在组的所有元素的组标识符变为q所在分组的标识符
        for (int i = 0; i < eleAndGroup.length; i++) {
            if (eleAndGroup[i]==pGroup){
                eleAndGroup[i] = qGroup;
            }
        }
        //分组个数-1
        this.count--;
    }
}
public class Test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请录入并查集中元素的个数:");
        int N = sc.nextInt();
        UF uf = new UF(N);
        while(true) {
            System.out.println("请录入要合并的第一个点:");
            int p = sc.nextInt();
            System.out.println("请录入要合并的第二个点:");
            int q = sc.nextInt();
            // 判断p和q是否在同一分组
            if (uf.connected(p,q)) {
                System.out.println("结点" + p + "结点" + q + "已经在同一分组");
                continue;
            }
            uf.union(p, q);
            System.out.println("总共还有" + uf.count() + "个分组");
        }
    }
}
请录入并查集中元素的个数:
5
请录入要合并的第一个点:
1
请录入要合并的第二个点:
3
总共还有4个分组
请录入要合并的第一个点:
1
请录入要合并的第二个点:
3
结点1结点3已经在同一分组
请录入要合并的第一个点:

2.4 应用举例与复杂性分析

如果将并查集中存储的每个整数表示一台网络中的计算机,就可以通过connected(int p, int q)来检测网络中的两台计算机是否连通,也可以通过union(int p, int q)使p和q连通,以便通信。

如果让每个数据都连通,至少要调用N-1次union方法,而在union方法中使用了for循环,因此时间复杂度是O(N^2),如果要解决大规模问题,不太合适,需要优化。

3 UF_Tree

3.1 算法优化

对eleAndGroup数组的含义重新设定:

  1. 索引依然是每个结点存储的元素;
  2. 索引对应的值不再是当前结点所在的分组标识,而是该结点的父结点。

在这里插入图片描述

3.2 find(int p)查询方法实现

  1. 判断p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根节点了;
  2. 如果eleAndGroup[p]不是自己,令p=eleAndGroup[p],继续找父结点的父结点,直到找到根结点为止,最终的根结点即为元素所在的分组标识。

在这里插入图片描述

3.3 union(int p, int q)合并方法实现

  1. 找到p所在树的根结点;
  2. 找到q所在树的根结点;
  3. 如果p和q拥有同样的根结点,则无需合并;
  4. 否则将p的根结点的父结点设置为q的根结点;
  5. 分组数量-1.

在这里插入图片描述
在这里插入图片描述

3.4 代码实现(Java)

public class UF_Tree {
    //记录结点元素和该元素所在分组的标识
    private int[] eleAndGroup;
    //记录并查集中数据的分组个数
    private int count;
    
    //初始化并查集
    public UF_Tree(int N){
        //初始化分组的数量,默认情况下,有N个分组
        this.count = N;
        //初始化eleAndGroup数组
        this.eleAndGroup = new int[N];
        //初始化eleAndGroup中的元素及其所在的组的标识符,让eleAndGroup数组的索引作为并查集的每个结点的元素,并且让每个索引处的值(该元素所在的组的标识符)等于该索引
        for (int i = 0; i < eleAndGroup.length; i++) {
            eleAndGroup[i] = i;
        }
    }

    //获取当前并查集中的数据有多少个分组
    public int count(){
        return count;
    }

    //判断并查集中元素p和元素q是否在同一分组中
    public boolean connected(int p,int q){
        return find(p) == find(q);
    }

    //元素p所在分组的标识符
    public int find(int p){
        while(true){
            if (p == eleAndGroup[p]){
                return p;
            }
            p = eleAndGroup[p];
        }
    }

    //把p元素所在分组和q元素所在分组合并
    public void union(int p,int q){
        //找到p元素和q元素所在组对应的树的根结点
        int pRoot = find(p);
        int qRoot = find(q);
        //如果p和q已经在同一分组,则不需要合并了
        if (pRoot==qRoot){
            return;
        }
        //让p所在的树的根结点的父结点为q所在树的根结点即可
        eleAndGroup[pRoot] = qRoot;
        //分组数量-1
        this.count--;
    }
}

3.5 优化后的复杂性分析

如果让每个数据都连通,要调用N-1次union方法,但是union调用了find算法,UF的find时间复杂度为O(1),现在UF_Tree的find平均时间复杂度为O(N/2),因此连通所有的时间复杂度是O(N^2/2),比之前好,但依然有优化空间。

4 UF_Tree_Weighted

4.1 路径压缩与复杂性分析

  1. 之所以UF_Tree连通所有的时间复杂度是O(N^2/2),原因就在于合并过程中p所在树的深度一直在增加,最终达到N,导致find搜索根节点耗费了较多时间,如果让树的深度尽可能小,就可以降低find的时间复杂度,甚至为O(1)。
  2. 在UF_Tree中,union合并树的时候过于随意,如果我们把并查集中每一棵树的大小记录下来,然后在合并树的时候,将较小的树连接到较大的树上,就可以减小树的深度,甚至能降到2。
  3. 如此,UF_Tree_Weighted连通所有的时间复杂度为O(N)

在这里插入图片描述

4.2 代码实现(Java)

为了能保证每次合并时,都能把小树合并到大树上,我们新增一个数组来记录每个根结点对应的树中元素的个数。

public class UF_Tree_Weighted {
    //记录结点元素和该元素所在分组的标识
    private int[] eleAndGroup;
    //记录并查集中数据的分组个数
    private int count;

    //用来存储每一个根结点对应的树中保存的结点个数
    private int[] sz;
    //初始化并查集
    public UF_Tree_Weighted(int N){
        //默认情况下,有N个分组
        this.count = N;
        //初始化eleAndGroup数组
        this.eleAndGroup = new int[N];
        //初始化eleAndGroup中的元素及其所在的组的标识符,让eleAndGroup数组的索引作为并查集的每个结点的元素,并且让每个索引处的值(该元素所在的组的标识符)等于该索引
        for (int i = 0; i < eleAndGroup.length; i++) {
            eleAndGroup[i] = i;
        }
        this.sz = new int[N];
        //默认情况下,sz中每个索引处的值都是1
        for (int i = 0; i < sz.length; i++) {
            sz[i] = 1;
        }
    }

    //获取当前并查集中的数据有多少个分组
    public int count(){
        return count;
    }

    //判断并查集中元素p和元素q是否在同一分组中
    public boolean connected(int p,int q){
        return find(p) == find(q);
    }

    //元素p所在分组的标识符
    public int find(int p){
        while(true){
            if (p == eleAndGroup[p]){
                return p;
            }
            p = eleAndGroup[p];
        }

    }

    //把p元素所在分组和q元素所在分组合并
    public void union(int p,int q){
        //找到p元素和q元素所在组对应的树的根结点
        int pRoot = find(p);
        int qRoot = find(q);

        //如果p和q已经在同一分组,则不需要合并了
        if (pRoot==qRoot){
            return;
        }

        //判断proot对应的树大还是qroot对应的树大,最终需要把较小的树合并到较大的树中
        if (sz[pRoot] < sz[qRoot]){
            eleAndGroup[pRoot] = qRoot;
            sz[qRoot] += sz[pRoot];
        }else{
            eleAndGroup[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }

        //分组数量-1
        this.count--;
    }
}

5 案例-畅通工程

5.1 题目要求

  1. 某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇;
  2. 畅通工程的目标是使全省任何两个城镇间都可以实现交通(不一定直接相连,间接相连即可);
  3. 问最少还需建设多少条道路?

有文件traffic_project.txt,下面是文件内容及数据解释:

在这里插入图片描述

5.2 解题思路

  1. 创建一个并查集UF_Tree_Weighted(20);
  2. 分别调用union(0,1),union(6,9),uinon(3,8),union(5,11),union(2,12),union(6,10),union(4,8),表示已经相连的城市;
  3. 如果城市全部连接起来,并查集中的分组数目为1,所有城市都在一个树中,因此,只需获取当前并查集中的分组数目,减去1,即得到至少还需要修建的道路数。

5.3 代码实现(Java)

public class Traffic_Project {
    public static void main(String[] args) throws IOException {
        // 创建输入流
        BufferedReader reader = new BufferedReader(new FileReader("src/traffic_project.txt"));
        // 读取城市数目,初始化并查集
        int number = Integer.parseInt(reader.readLine());
        UF_Tree_Weighted uf = new UF_Tree_Weighted(number);
        // 读取已经修建好的道路数目
        int roadNumber = Integer.parseInt(reader.readLine());
        // 循环读取已经修建好的道路,并调用union方法
        for (int i=0; i<roadNumber; i++) {
            String line = reader.readLine();
            int p = Integer.parseInt(line.split(" ")[0]);
            int q = Integer.parseInt(line.split(" ")[1]);
            uf.union(p, q);
        }

        // 获取剩余的分组数量
        int groupNumber = uf.count();
        // 计算出还需要修建的道路
        System.out.println("还需要修建" + (groupNumber-1) + "条道路,城市才能相通");
    }
}
还需要修建12条道路,城市才能相通
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

hellosc01

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

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

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

打赏作者

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

抵扣说明:

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

余额充值