并查集

简介: 并查集是一种高效的集合存储结构,擅长处理集合的合并与查询操作。它通过树形结构将每个集合视为一棵树,整个并查集则是一片森林。利用一维数组实现,数组下标代表元素,值代表其父节点,根节点指向自身。合并操作通过连接两个集合的根节点完成,查询操作则通过查找根节点确定元素所属集合。并查集广泛应用于图论、网络连接等问题中。

数据结构里汇集了很多种存储结构,包括线性表、树、图等,它们还可以进行更细致地划分,每一种结构都有其特定的用途和优势。


拿最简单的线性表举例,它专门存储逻辑关系是“一对一”的数据。线性表又可以细分为顺序表和链表,顺序表擅长对目标元素的查询操作,而链表擅长对目标元素的添加(插入)和删除操作。


本节给大家讲解的并查集,也是一种存储结构,它可以存储多个集合,特别擅长处理集合的合并(Union)和查询(Find)操作。


这里提到的集合,与数学里的集合非常类似,可以存放一组元素,它的特点是:

  • 集合中的每个元素都是独一无二的,集合里不允许存在重复的元素;
  • 集合中的元素是没有顺序的。


例如,{1, 2, 3} 是一个集合,它和{3, 1, 2}是同一个集合,因为它们包含相同的元素。


集合的合并操作,指的是将两个独立的集合整合成一个大的集合,比如将 {1, 2} 和 {3} 合并为 {1, 2, 3};集合的查询操作,指的是查找目标元素属于哪个集合。


搞清楚并查集是什么之后,接下来讲解并查集的具体实现。

并查集的实现

并查集可以存储多个集合,它能够高效地完成集合的合并和查询操作。


给定一个并查集,我们可以把它里面的每个集合想象成一棵树结构(注意不是二叉树,是多叉树),那么整个并查集就是一个森林。假设{0, 1, 2, 3, 4} {5, 6, 7} {8}是一个并查集,它是一个包含 3 棵树的森林,如下图所示:


图 1 想象成森林的并查集


在图 1 的基础上,可以很轻松地完成集合的合并和查询操作:

1) 合并两个集合,只需要找到它们各自的树根结点,再把两个树根结点相连即可,如下图所示:


图 2 集合的合并操作

图中,r 指的是整棵树的高度(深度);s 指的是树中的结点数量。

像图 2 这样,通过连接两棵树的根结点,把两棵树整合成一棵树,就实现了把两个集合{0, 1, 2, 3, 4} {5, 6, 7}合并成一个集合{0, 1, 2, 3, 4, 5, 6, 7}


2) 查询某个元素位于哪个集合,等同于确定该元素位于哪棵树上。我们习惯选择树根结点代表整棵树(把位于树根的元素作为整个集合的代表),因为只要树的根结点存在,整棵树就一直存在。


仔细观察图 2,两棵树未整合之前,元素 5 位于根结点是 6 的树上(5 位于代表元素是 6 的集合里);两棵树整合之后,元素 5 所在的树成为了另一棵树的一部分,元素 5 就位于根结点是 3 的树上(5 位于代表元素是 3 的集合里)。


在上述的讲解中,我们把并查集想象成是森林,把并查集里的集合想象成一棵棵的树,实现了集合的合并和查询操作。然而真正落地、写代码实现并查集,用的不是复杂的树形结构,而是顺序表(一维数组)。


假设并查集内有 3 个集合,分别是{0, 1, 2, 3, 4}、{5, 6, 7} 和 {8}。想要将这 3 个集合存放到数组里,除了存储所有元素的值之外,还要存储各个元素之间的关系,存储方案是:

  • 将并查集里的每个元素各自对应一个数组下标。换句话说,用数组下标代表并查集里的元素;
  • 把每个集合想象成一棵树,数组中负责记录各个元素的父亲结点的位置下标。树根结点比较特殊,它没有父亲结点,它对应的数组空间里存储的是自己的位置下标。


存储{0, 1, 2, 3, 4} {5, 6, 7} {8}的并查集如下图所示:


图 3 存放多个集合的数组


仔细观察图 3 下方的数组:

  • 下标 0 处存储的值是 2,表明元素 0 和 2 位于同一个集合里;
  • 下标 1 存储的值是 3,表明 1 和 3 位于同一个集合;
  • 下标 2 存储的值是 3,表明 2 和 3 位于同一个集合;
  • 下标 3 存储的值是 3,它存储的是自己的下标值,表明它是一个树根结点,换句话说,它是一个集合的代表元素;
  • 下标 4 存储的值是 3,表明 4 和 3 位于同一个集合。


0 和 2、2 和 3、1 和 3、4 和 3 都位于同一个集合,表明 {0, 1, 2, 3, 4} 是并查集中的一个集合,并且这个集合的代表元素是 3。同样的道理,可以得出 {5, 6, 7} 是一个集合,代表元素是 6;{8} 是一个集合,代表元素是 8。


你看,一维数组既存储了并查集中的全部元素,又存储了元素之间的关系,确实成功地存储了一个并查集。并查集可以用如下的 C 语言结构体表示:

  1. typedef struct  {
  2. int parent[MAX_SIZE];
  3. int count;
  4. }UnionFind;

parent[] 数组用来存储并查集,count 用来统计并查集中的集合数量。


那么,如何在一维数组中实现集合的合并和查询操作呢?


仔细观察图 2,合并两个集合的过程,其实只做了一件事,就是把两个集合的代表元素(树根结点)连接起来。因此,合并数组中的两个集合,只需要做两件事:

  • 找到两个集合中的代表元素,假设是 x 和 y;
  • 把 x 的父结点改成 y,或者把 y 的父结点改成 x,都可以实现 x 和 y 建立连接。
相关文章
|
4月前
|
算法 Java C语言
弗洛伊德算法求最短路径
弗洛伊德算法用于寻找加权图中各顶点间的最短路径,适用于无向图和有向图。算法基于动态规划思想,通过枚举中间顶点来更新路径,确保最终得到最短路径。该算法要求路径权值非负,否则可能出错。
383 0
|
4月前
|
存储 搜索推荐 算法
归并排序算法
归并排序是一种基于分治思想的高效排序算法,通过将序列不断划分为不可再分的子序列,再两两合并完成排序。其时间复杂度为O(nlogn),适用于对序列进行升序或降序排列。
272 0
|
4月前
|
存储 算法 Java
求数组中的最大值和最小值
本文介绍了在程序中如何查找数组中的最大值和最小值,重点讲解了两种算法:普通算法和分治算法。普通算法通过遍历数组直接比较元素大小,找出最值;而分治算法则通过递归将数组划分成更小的部分,分别找出各部分的最大值,最终合并结果得到整个数组的最大值。文章以 {3,7,2,1} 为例,详细演示了两种算法的实现过程,并提供了 C、Java 和 Python 的代码示例。
267 0
|
4月前
|
算法 搜索推荐
分治算法的基本思想
分治算法通过将复杂问题拆分成多个独立的小问题,逐一解决后再合并结果。适合处理大规模数据,常见应用包括排序、查找最大值/最小值及汉诺塔等问题。
123 0
|
4月前
|
算法 Java 定位技术
迷宫问题
迷宫问题是指在给定区域内寻找从起点到终点的可行路径。可以使用回溯算法解决,通过不断尝试四个方向(上下左右)移动,若无法前进则回退,直到找到终点或遍历所有可能路径。文中还给出了C语言、Java和Python的实现代码,并展示了运行结果。
164 0
|
4月前
|
算法 程序员
时间复杂度和空间复杂度的概念
本文介绍了如何评估算法的执行效率和内存占用,重点讲解了时间复杂度和空间复杂度的概念及其计算方法。通过大O记法,可以量化算法的运行时间和内存使用情况,从而在不同算法间做出合理选择。
136 0
|
4月前
|
存储 算法
最小生成树的概念与思想
数据结构中的图存储结构包含顶点和边,分为连通图与非连通图。生成树是包含所有顶点、任意两点间仅有一条通路的极小连通图。最小生成树则是权值总和最小的生成树,常用于解决道路建设等实际问题,常用算法有普里姆算法和克鲁斯卡尔算法。
105 0
|
4月前
|
存储 算法 Java
寻找图中是否存在路径
本节介绍了如何使用并查集判断图中两个顶点之间是否存在有效路径。通过将图的边信息存储到并查集中,同一连通区域的顶点会处于同一集合。只需比较两个顶点的根节点是否相同,即可判断它们是否连通。文中提供了C语言、Java和Python的实现代码,并通过示例验证了算法的正确性。
117 0
|
4月前
|
算法 Java C语言
汉诺塔问题
汉诺塔问题源自印度古老传说,涉及将一组圆盘从一根柱子移动到另一根,遵循特定规则。文章详细介绍了该问题的背景、解决思路以及如何使用分治算法实现,同时提供了C语言、Java和Python的代码示例。
282 0