数据结构与算法之并查集

简介: 数据结构与算法之并查集

并查集


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


查询元素 p和元素q是否属于同一组

合并元素 p和元素q所在的组

cc7d84b3052d4fd6b3e0870aaec197ab.png

1.1.并查集结构


并查集也是一种树型结构,但这棵树跟我们之前讲的二叉树、红黑树、B树等都不一样,这种树的要求比较简单:


1.每个元素都唯一的对应一个结点;

2.每一组数据中的多个元素都在同一颗树中;

3.一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系;

4.元素在树中并没有子父级关系的硬性要求;


ab297dc59e734cfc9acd734de14a7612.png

1.2 并查集API设计


image.png


1.3 并查集的实现


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


1.初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组;


2.初始化数组eleAndGroup;


3.把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点


所在的分组,那么初始化情况下,i索引处存储的值就是i


9606b45e0e2840deaceafd460c6662ad.png


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


1.如果p和q已经在同一个分组中,则无需合并

2.如果p和q不在同一个分组,则只需要将p元素所在组的所有的元素的组标识符修改为q元素所在组的标识符即可

3.分组数量-1


a77437409283425a98adfd58f0631b1c.png

1.3.3 代码

// 并查集代码
public class UF {
  //记录结点元素和该元素所在分组的标识
  private int[] eleAndGroup;
  //记录并查集中数据的分组个数
  private int count;
  //初始化并查集
  public UF(int N){
    //初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
    this.count=N;
    //初始化数组
    eleAndGroup = new int[N];
    //把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点所在的分组,那么初始化情况下, i索引处存储的值就是i
    for (int i = 0; i < N; 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){
    //如果p和q已经在同一个分组中,则无需合并;
    if (connected(p,q)){
      return;
    }
    //如果p和q不在同一个分组,则只需要将p元素所在组的所有的元素的组标识符修改为q元素所在组的标识符即可
    int pGroup = find(p);
    int qGroup = find(q);
    for (int i = 0; i < eleAndGroup.length; i++) {
      if (eleAndGroup[i]==pGroup){
        eleAndGroup[i]=qGroup;
      }
    }
    //分组数量-1
    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()+"个分组");
    }
  }
}

1.3.4 并查集应用举例


如果我们并查集存储的每一个整数表示的是一个大型计算机网络中的计算机,则我们就可以通过connected(int p,int q)来检测,该网络中的某两台计算机之间是否连通?如果连通,则他们之间可以通信,如果不连通,则不能通信,此时我们又可以调用union(int p,int q)使得p和q之间连通,这样两台计算机之间就可以通信了。


一般像计算机这样网络型的数据,我们要求网络中的每两个数据之间都是相连通的,也就是说,我们需要调用很多次union方法,使得网络中所有数据相连,其实我们很容易可以得出,如果要让网络中的数据都相连,则我们至少要调用N-1次union方法才可以,但由于我们的union方法中使用for循环遍历了所有的元素,所以很明显,我们之前实现的合并算法的时间复杂度是O(N^2),如果要解决大规模问题,它是不合适的,所以我们需要对算法进行优化。


1.3.5 UF_Tree 算法优化


为了提升union算法的性能,我们需要重新设计find方法和union方法的实现,此时我们先需要对我们的之前数据结


构中的eleAndGourp数组的含义进行重新设定:


1.我们仍然让eleAndGroup数组的索引作为某个结点的元素;

2.eleAndGroup[i]的值不再是当前结点所在的分组标识,而是该结点的父结点;


58bc32e518fc4ab7844def141131af6f.png


1.3.5.1 UF_Tree API 设计


image.png

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


1.判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;

2.如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根结点为止;


e576aec7fbc24c50958c8c523e429456.png

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


1.找到p元素所在树的根结点

2.找到q元素所在树的根结点

3.如果p和q已经在同一个树中,则无需合并;

4.如果p和q不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可;

5.分组数量-1


c957d396beb94c3b9ca388e083ed3f4f.png


1.3.5.4 代码


public class UF_Tree {
  //记录结点元素和该元素所的父结点
  private int[] eleAndGroup;
  //记录并查集中数据的分组个数
  private int count;
  //初始化并查集
  public UF_Tree(int N){
    //初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
    this.count=N;
    //初始化数组
    eleAndGroup = new int[N];
    //把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点的父结点,那么初始化情况下,i索引处存储的值就是i
    for (int i = 0; i < N; i++) {
      eleAndGroup[i]=i;
    }
  }
  //获取当前并查集中的数据有多少个分组
  public int count(){
    return count;
  }
  //元素p所在分组的标识符
  public int find(int p){
    while(true){
      //判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;
      if (p==eleAndGroup[p]){
        return p;
      }
      //如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根结点为止;
      p=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){
    //找到p元素所在树的根结点
    int pRoot = find(p);
    //找到q元素所在树的根结点
    int qRoot = find(q);
    //如果p和q已经在同一个树中,则无需合并;
    if (pRoot==qRoot){
      return;
    }
    //如果p和q不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可;
    eleAndGroup[pRoot]=qRoot;
    //分组数量-1
    count--;
  }
} 


1.3.5.5 优化后的性能分析


我们优化后的算法union,如果要把并查集中所有的数据连通,仍然至少要调用N-1次union方法,但是,我们发现union方法中已经没有了for循环,所以union算法的时间复杂度由O(N^2)变为了O(N)。


但是这个算法仍然有问题,因为我们之前不仅修改了union算法,还修改了find算法。我们修改前的find算法的时间复杂度在任何情况下都为O(1),但修改后的find算法在最坏情况下是O(N)


b33c12ea12d049fab5c2801adf4522a6.png

在 union方法中调用了find方法,所以在最坏情况下union算法的时间复杂度仍然为O(N^2)。


1.3.6 路径压缩


UF_Tree中最坏情况下union算法的时间复杂度为O(N^2),其最主要的问题在于最坏情况下,树的深度和数组的大小一样,如果我们能够通过一些算法让合并时,生成的树的深度尽可能的小,就可以优化find方法。


之前我们在union算法中,合并树的时候将任意的一棵树连接到了另外一棵树,这种合并方法是比较暴力的,如果我们把并查集中每一棵树的大小记录下来,然后在每次合并树的时候,把较小的树连接到较大的树上,就可以减小树的深度。


ecb8df6cafcc475199ca5578e9cbf969.png


只要我们保证每次合并,都能把小树合并到大树上,就能够压缩合并后新树的路径,这样就能提高 find方法的效率。为了完成这个需求,我们需要另外一个数组来记录存储每个根结点对应的树中元素的个数,并且需要一些代码调整数组中的值。


1.3.6.1 UF_Tree_Weighted API设计


image.png


1.3.6.2 代码

public class UF_Tree_Weighted {
  //记录结点元素和该元素所的父结点
  private int[] eleAndGroup;
  //存储每个根结点对应的树中元素的个数
  private int[] sz;
  //记录并查集中数据的分组个数
  private int count;
  //初始化并查集
  public UF_Tree_Weighted(int N){
    //初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
    this.count=N;
    //初始化数组
    eleAndGroup = new int[N];
    sz = new int[N];
    //把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点的父结点,那么初始化情况下,i索引处存储的值就是i
    for (int i = 0; i < N; i++) {
      eleAndGroup[i]=i;
    }
    //把sz数组中所有的元素初始化为1,默认情况下,每个结点都是一个独立的树,每个树中只有一个元素
    for (int i = 0; i < sz.length; i++) {
      sz[i]=1;
    }
  }
  //获取当前并查集中的数据有多少个分组
  public int count(){
    return count;
  }
  //元素p所在分组的标识符
  public int find(int p){
    while(true){
      //判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;
      if (p==eleAndGroup[p]){
        return p;
      }
      //如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到  找到根结点为止;
      p=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){
    //找到p元素所在树的根结点
    int pRoot = find(p);
    //找到q元素所在树的根结点
    int qRoot = find(q);
    //如果p和q已经在同一个树中,则无需合并;
    if (pRoot==qRoot){
      return;
    }
    //如果p和q不在同一个分组,比较p所在树的元素个数和q所在树的元素个数,把较小的树合并到较大的树上
    if (sz[pRoot]<sz[qRoot]){
      eleAndGroup[pRoot] = qRoot;
      //重新调整较大树的元素个数
      sz[qRoot]+=sz[pRoot];
    }else{
      eleAndGroup[qRoot]=pRoot;
      sz[pRoot]+=sz[qRoot];
    }
    //分组数量-1
    count--;
  }
}

1.3.7 案例-畅通工程


某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。



问最少还需要建设多少条道路?


在我们的测试数据文件夹中有一个trffic_project.txt文件,它就是诚征道路统计表,下面是对数据的解释:


总共有 20个城市,目前已经修改好了7条道路,问还需要修建多少条道路,才能让这20个城市之间全部相通?


解题思路:


1.创建一个并查集UF_Tree_Weighted(20);


2.分别调用union(0,1),union(6,9),union(3,8),union(5,11),union(2,12),union(6,10),union(4,8),表示已经修建好的道路把对应的城市连接起来;


3.如果城市全部连接起来,那么并查集中剩余的分组数目为1,所有的城市都在一个树中,所以,只需要获取当前并查集中剩余的数目,减去1,就是还需要修建的道路数目;


代码:


public class Traffic_Project {
  public static void main(String[] args)throws Exception {
    //创建输入流
    BufferedReader reader = new BufferedReader(new
    InputStreamReader(Traffic_Project.class.getClassLoader().getResourceAsStream("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)+"道路,城市才能相通");
  }
}


image.png

目录
相关文章
|
3月前
|
算法 开发者 计算机视觉
燃爆全场!Python并查集:数据结构界的网红,让你的代码炫酷无比!
在编程的世界里,总有一些数据结构以其独特的魅力和高效的性能脱颖而出,成为众多开发者追捧的“网红”。今天,我们要介绍的这位明星,就是Python中的并查集(Union-Find)——它不仅在解决特定问题上大放异彩,更以其优雅的设计和强大的功能,让你的代码炫酷无比,燃爆全场!
41 0
|
4月前
|
算法 JavaScript 前端开发
第一个算法项目 | JS实现并查集迷宫算法Demo学习
本文是关于使用JavaScript实现并查集迷宫算法的中国象棋demo的学习记录,包括项目运行方法、知识点梳理、代码赏析以及相关CSS样式表文件的介绍。
第一个算法项目 | JS实现并查集迷宫算法Demo学习
|
3月前
|
存储 算法 Python
火箭般的提升!学会Python并查集,让你的算法能力飞跃新高度!
火箭般的提升!学会Python并查集,让你的算法能力飞跃新高度!
38 1
|
4月前
|
Python
逆天改命!掌握Python并查集,数据结构难题从此不再是你的痛!
在编程旅程中,遇到棘手的数据结构难题是否让你苦恼?别担心,Python并查集(Union-Find)是你的得力助手。这是一种高效处理不相交集合合并及查询的数据结构,广泛应用于网络连通性、社交网络圈子划分等场景。通过维护每个集合的根节点,它实现了快速合并与查询。本文将介绍并查集的基本概念、应用场景以及如何在Python中轻松实现并查集,帮助你轻松应对各种数据结构挑战。
41 3
|
4月前
|
算法 计算机视觉 Python
Python并查集大揭秘:让你在算法界呼风唤雨,秒杀一切复杂场景!
在编程与算法的广袤天地中,总有一些工具如同神兵利器,能够助你一臂之力,在复杂的问题前游刃有余。今天,我们就来深入探讨这样一件神器——Python并查集(Union-Find),看看它是如何让你在算法界呼风唤雨,轻松应对各种复杂场景的。
82 2
|
4月前
|
存储 C语言
数据结构基础详解(C语言): 树与二叉树的应用_哈夫曼树与哈夫曼曼编码_并查集_二叉排序树_平衡二叉树
本文详细介绍了树与二叉树的应用,涵盖哈夫曼树与哈夫曼编码、并查集以及二叉排序树等内容。首先讲解了哈夫曼树的构造方法及其在数据压缩中的应用;接着介绍了并查集的基本概念、存储结构及优化方法;随后探讨了二叉排序树的定义、查找、插入和删除操作;最后阐述了平衡二叉树的概念及其在保证树平衡状态下的插入和删除操作。通过本文,读者可以全面了解树与二叉树在实际问题中的应用技巧和优化策略。
|
4月前
|
Python
告别低效!Python并查集:数据结构界的超级英雄,拯救你的编程人生!
告别低效!Python并查集:数据结构界的超级英雄,拯救你的编程人生!
34 0
|
4月前
|
算法 开发者 计算机视觉
Python并查集:数据结构界的肌肉男,让你在编程路上无所畏惧!
在编程的浩瀚宇宙中,数据结构如同基石,构建了解决问题的坚实框架。而并查集(Union-Find),这位数据结构界的“肌肉男”,以其独特的魅力和强大的功能,让无数开发者在面对复杂关系处理时,都能感受到前所未有的从容与自信。今天,就让我们一同揭开并查集的神秘面纱,看看它是如何成为你编程路上的得力助手的。
37 0
|
4月前
|
算法 程序员 计算机视觉
Python并查集:数据结构界的肌肉男,让你在编程路上无所畏惧!
并查集,一种处理不相交集合合并与查询的数据结构,被誉为编程的“肌肉男”。它提供Find(找根节点)和Union(合并集合)操作,常用于好友关系判断、图像处理、集合合并等。Python实现中,路径压缩和按秩合并优化效率。并查集的高效性能使其成为解决问题的强大工具,助力程序员应对复杂挑战。
37 0
|
6月前
|
算法 程序员 图形学
脑洞大开!Python并查集:用最简单的方式,解决最复杂的数据结构问题!
【7月更文挑战第17天】并查集,数据结构明星,处理不相交集合合并与查询。Python实现核心操作:查找与合并。路径压缩优化查找,按秩合并保持平衡。实战应用如图连通性判断,算法竞赛利器。掌握并查集,解锁复杂问题简单解法,照亮编程之旅!
65 10