数据结构——并查集

article/2025/8/27 0:22:56

并查集是一种数据结构,是树的一种应用,用于处理一些不交集(一系列没有重复元素的集合)的合并以及查询问题。并查集支持如下操作:

  • 查询:查询某个元素属于哪个集合,通常是返回集合内的一个“代表元素”。这个操作一般是为了判断两个元素是否在同一个集合之中。
  • 合并:将两个集合合并为一个。
  • 添加:添加一个新集合,其中有一个新元素。不如查询和合并操作重要。

我们来看下面一个比较有趣的并查集应用的例子:

话说江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,江湖上就形成了一个一个的群落,通过两两之间的朋友关系串联起来。而不在同一个群落的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢?

我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物,这样,每个圈子就可以这样命名“齐达内朋友之队”“罗纳尔多朋友之队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。如此一来,门派便产生了。

在上面的例子中,我们可以认为每个门派都是一个集合,门派的掌门人作为集合的“代表元素”,通过确认双方的掌门人是否是同一个人来确定对方是否是自己人。对于没有门派的大侠,我们也可以认为他自己组成一个门派,掌门人就是他自己。集合的合并就类似于门派的合并,门派合并后需要重新推出一个掌门人,也就是要重新选取集合的“代表元素”。

实现方法一

实现方法一总是记住一个集合中编号最小的元素,类似一个门派中的大侠总是记住自己的掌门人是谁。

  • 我们定义一个数组set[1…n],其中set[i]表示元素i所在集合;
  • 用编号最小的元素标记所在集合。

我们来看下面一个例子,假设有以下不相交集合:

{1, 3, 7}, {4}, {2, 5, 9 10}, {6, 8}

数组set[1…n]为:

i12345678910
set[i]1214261622

其中1, 3, 7组成一个集合,他们中间最小为1,因此set[1]、set[3]、set[7]都为1。

假设我们现在要将{1, 3, 7}、{2, 5, 9 10}这两个集合合并,合并方式如下:
方法一合并过程

数组set[1…n]更新为:

i12345678910
set[i]1114161611

代码实现

初始化

初始时我们传入元素个数,并将set[i]设置为自身。

    private int[] set;/*** 构造方法,初始化set数组** @param num 元素数量*/public MergeFindSet1(int num) {set = new int[num];for (int i = 0; i < num; i++) {// 初始时初始化为自身set[i] = i;}}

查找方法

查找方法比较简单,由于set中记录的永远是集合当中的代表元素,因此只需要返回set[i]即可:

    /*** 查询某个元素属于哪个集合** @param num* @return*/public int find(int num) {return set[num];}

合并方法

在并查集的合并方法中,我们传入两个元素的编号,代表这两个元素在同一个集合当中。我们先找到两个元素所在集合的代表元素,接着找两个代表元素中编号大的那个,将编号大的元素的set[max]赋值为编号小代表元素min。同时由于方法一总是记住的是集合当中编号最小的,因此还需要更新set[i]=max的所有元素。

    /*** 集合的合并操作** @param a* @param b*/public void merge(int a, int b) {int findA = find(a);int findB = find(b);int max = Math.max(findA, findB);int min = Math.min(findA, findB);// 用编号小的元素标记所在集合// 同时还需要把以前同一个集合中的元素的set[i]更新for (int i = 0; i < set.length; i++) {if (set[i] == max) {set[i] = min;}}}

测试

    public static void main(String[] args){MergeFindSet1 mergeFindSet1 = new MergeFindSet1(11);mergeFindSet1.merge(1, 3);mergeFindSet1.merge(3, 7);mergeFindSet1.merge(2, 5);mergeFindSet1.merge(5, 9);mergeFindSet1.merge(9, 10);mergeFindSet1.merge(6, 8);for (int i = 0; i < 11; i++) {System.out.print(mergeFindSet1.find(i) + " ");}System.out.println("\n==={1, 3, 7}和{2, 5, 9 10}合并后===");mergeFindSet1.merge(3, 9);for (int i = 0; i < 11; i++) {System.out.print(mergeFindSet1.find(i) + " ");}}

得到的输出结果为:

0 1 2 1 4 2 6 1 6 2 2 
==={1, 3, 7}和{2, 5, 9 10}合并后===
0 1 1 1 4 1 6 1 6 1 1

和我们上面的推论一致。

实现方法二

在实现方法一中,合并方法需要搜索全部的元素,时间复杂度为O(n),效率并不是很高。

我们可以使用树结构去改造并查集的结构,对于集合中的元素,只需要记住它的上级节点。类似以下这种结构:
树结构
定义数组set[1…n]:

  • set[i]=i,则i表示本集合,并是集合对应树的根。以门派为例子,i是门派的掌门人。
  • set[i]=j, j!=i,则j是i的父节点,以门派为例,j是i的上级。

我们以上面的例子去构造set数组,set数组为:

i12345678910
set[i]1232134334

假设我们现在需要将上图的门派A和门派B进行合并,合并后的门派A+B如下:
方法二合并过程
set数组更新为:

i12345678910
set[i]1132134334

代码实现

初始化

和上面方法1一样,初始化set数组,初始时set[i]都赋值为自身。

    private int[] set;public MergeFindSet2(int num) {set = new int[num];for (int i = 0; i < num; i++) {// 初始时初始化为自身set[i] = i;}}

查找方法

方法二查找元素所属集合就是树中的查找根节点的方式。

    public int find(int num) {while (set[num] != num) {num = set[num];}return num;}

合并方法

使用方法二进行集合合并时,以门派为例,只需要把其中一个掌门的掌门人信息更新即可。

    public void merge(int a, int b) {int findA = find(a);int findB = find(b);if (findA > findB) {set[findA] = findB;} else {set[findB] = findA;}}

测试

    public static void main(String[] args) {MergeFindSet2 mergeFindSet2 = new MergeFindSet2(11);mergeFindSet2.merge(1, 5);mergeFindSet2.merge(4, 7);mergeFindSet2.merge(4, 10);mergeFindSet2.merge(2, 4);mergeFindSet2.merge(3, 6);mergeFindSet2.merge(3, 8);mergeFindSet2.merge(3, 9);for (int i : mergeFindSet2.set) {System.out.print(i + " ");}System.out.println();for (int i = 0; i < 11; i++) {System.out.print(mergeFindSet2.find(i) + " ");}System.out.println("\n===========合并帮派A和B后=========");mergeFindSet2.merge(5, 7);for (int i : mergeFindSet2.set) {System.out.print(i + " ");}System.out.println();for (int i = 0; i < 11; i++) {System.out.print(mergeFindSet2.find(i) + " ");}}

得到的输出结果为:

0 1 2 3 2 1 3 4 3 3 4 
0 1 2 3 2 1 3 2 3 3 2 
===========合并帮派A和B后=========
0 1 1 3 2 1 3 4 3 3 4 
0 1 1 3 1 1 3 1 3 3 1 

其中第一行和第四为set数组,和我们上面的推论相同。

合并方法优化

方法二在合并的时候平均时间复杂度为O(logn),相比方法一得到了改善。但是如果树为单支树,查找的时间复杂度会退化为O(n),依然存在效率问题。

方法三对merge方法进行了优化,在进行merge前会先获取两棵树的高度,merge时将高度小的树合并到高度大的树,在一定程度上维持了平衡,避免了单支树的情况。以帮派的例子为例,假如帮派A的掌门人是a,帮派B的掌门人是b,如果帮派A和帮派B要握手言和,合并为一个帮派,那么只需要让掌门a认掌门b为上级,或者让掌门b认掌门a为上级即可。那究竟是掌门a上位还是掌门b上位呢?那就要看哪个帮派的成员结构更复杂,形成的树更高了。

合并方法如下:
在这里插入图片描述
假设两棵树的高度分别为h1和h2,则合并后树的高度是:

  • h1 != h2:max(h1, h2)
  • h1 == h2:h1 + 1

代码实现

初始化

对比前两种方法,方法三需要增加一个height数组用于存储树的高度,height[i]表示以i为根节点的树的高度。

    int[] set;int[] height;public MergeFindSet3(int num) {set = new int[num];height = new int[num];for (int i = 0; i < num; i++) {// 初始时初始化为自身set[i] = i;// 高度初始化为1height[i] = 1;}}

合并方法和查询方法

这里查询方法和方法二的一样,在merge之前会先判断元素所在树的高度,根据树的高度决定如何挂树。

    public void merge(int a, int b) {int aRoot = find(a);int bRoot = find(b);// 两棵树高度相等时,随便挑一个作为根节点就行if (height[aRoot] == height[bRoot]) {set[bRoot] = aRoot;height[aRoot] = height[aRoot] + 1;}// 如果a所在子树的高度比b的高,把b挂到a,a的高度不变else if (height[aRoot] > height[bRoot]) {set[bRoot] = aRoot;}// b所在子树高度比a高,把a挂到belse {set[aRoot] = bRoot;}}public int find(int num) {while (set[num] != num) {num = set[num];}return num;}

测试

    public static void main(String[] args) {MergeFindSet3 mergeFindSet3 = new MergeFindSet3(11);mergeFindSet3.merge(1, 5);mergeFindSet3.merge(2, 4);mergeFindSet3.merge(4, 7);mergeFindSet3.merge(4, 10);mergeFindSet3.merge(3, 6);mergeFindSet3.merge(3, 8);mergeFindSet3.merge(3, 9);System.out.print("index: ");for (int i = 0; i < 11; i++) {System.out.print(i + " ");}System.out.println();System.out.print("  set: ");for (int i : mergeFindSet3.set) {System.out.print(i + " ");}System.out.println();System.out.print(" root: ");for (int i = 0; i < 11; i++) {System.out.print(mergeFindSet3.find(i) + " ");}System.out.println("\n合并5和7所在集合后:");mergeFindSet3.merge(5, 7);System.out.print("  set: ");for (int i : mergeFindSet3.set) {System.out.print(i + " ");}System.out.println();System.out.print(" root: ");for (int i = 0; i < 11; i++) {System.out.print(mergeFindSet3.find(i) + " ");}}

得到的输出结果为:

index: 0 1 2 3 4 5 6 7 8 9 10 set: 0 1 2 3 2 1 3 2 3 3 2 root: 0 1 2 3 2 1 3 2 3 3 2 
合并5和7所在集合后:set: 0 1 1 3 2 1 3 2 3 3 2 root: 0 1 1 3 1 1 3 1 3 3 1

路径压缩算法

路径压缩算法是对find方法的优化。考虑这样的场景,门派的结构非常复杂,门派中的每个大侠又都只知道自己的上级是谁,并不知道自己的掌门是谁,当两个大侠碰面时,他们需要一级一级向上询问来确定掌门人,这样的查找效率就比较低下。最理想的结构是所有人都知道掌门人是谁,这样就产生了路径压缩算法。当大侠找到自己的掌门是谁后,就记住自己的掌门,下次直接找掌门再确认就好。

以下图左边的树为例,假如我们要查找元素5和元素7的根节点,压缩后的路径如下:
路径压缩

代码实现

路径压缩算法实现的find方法

    public int find2(int num) {// 查找根节点int root = num;while (set[root] != root) {root = set[root];}// 将路径上经过的元素都指向根节点while (set[num] != root) {int parent = set[num];set[num] = root;num = parent;}return root;}

测试

我们以下图为例测试方法的正确性:
在这里插入图片描述
如果路径正确压缩,最终的树结构为:
在这里插入图片描述

    public static void main(String[] args) {MergeFindSet2 mergeFindSet2 = new MergeFindSet2(11);mergeFindSet2.merge(1, 5);mergeFindSet2.merge(2, 4);mergeFindSet2.merge(4, 7);mergeFindSet2.merge(4, 10);mergeFindSet2.merge(3, 6);mergeFindSet2.merge(3, 8);mergeFindSet2.merge(3, 9);System.out.println("===========初始时的set数组=========");for (int i : mergeFindSet2.set) {System.out.print(i + " ");}for (int i = 0; i < 11; i++) {mergeFindSet2.find2(i);}System.out.println("\n===========find一轮后的set数组=========");for (int i : mergeFindSet2.set) {System.out.print(i + " ");}}

得到的输出结果为:

===========初始时的set数组=========
0 1 2 3 2 1 3 4 3 3 4 
===========find一轮后的set数组=========
0 1 2 3 2 1 3 2 3 3 2 

http://chatgpt.dhexx.cn/article/Wpd1Kee0.shtml

相关文章

并查集详解(C/C++)

并查集算法详解&#xff08;C&#xff09; 并查集基础并查集是什么&#xff1f;并查集的作用是什么&#xff1f;并查集的结构合并查询 代码实现优化1&#xff1a;避免退化&#xff08;按秩合并&#xff09;代码优化 优化2&#xff1a;路径压缩代码优化 最终代码实现复杂度分析经…

并查集及其应用

并查集的基本操作: 1.合并两个集合 2.查询两个元素的祖宗节点 扩展: 1.记录每个集合的大小 将集合大小绑定到代表元素节点上 就是并查集的思想 (不是每个元素都是正确的 但是代表元素是正确的) 2.记录每个点到根节点的距离 绑定到每个元素身上 因为每个元素到根节点的距离…

并查集(Union-Find) (图文详解)

文章目录 并查集基础知识定义C实现优化 精选算法题(Java实现)实现并查集交换字符串中的元素最长连续序列 - 字节面试常考连通网络的操作次数最大岛屿数量 (三种解法)省份数量冗余连接冗余连接Ⅱ情侣牵手(困难)移除最多的同行或同列石头等式方程的可满足性 结语 并查集基础知识 …

什么是 “并查集” ?

导语&#xff1a;并查集是一种精巧的算法&#xff0c;本身并不难理解&#xff0c;却很常用&#xff0c;在许多场景下都能找到并查集的身影。 本文作者封承成&#xff0c;年仅12岁&#xff0c;非常感谢他的投稿。 并查集是什么 并查集&#xff0c;是一种判断“远房亲戚”的算法。…

并查集(Disjoint Set)

目录 ❤️什么是并查集&#xff1f; &#x1f3b6;实现方法1 &#x1f414;实现方法2 &#x1f383;题目1 ❤️什么是并查集&#xff1f; 并查集是一种数据结构&#xff0c;用于处理一些不交集&#xff08;Disjoint sets&#xff0c;一系列没有重复元素的集合&#xff09;的…

并查集

参考 https://www.cnblogs.com/asdfknjhu/p/12515480.html https://www.bilibili.com/video/BV13t411v7Fs?p3 https://leetcode-cn.com/problems/number-of-provinces/solution/python-duo-tu-xiang-jie-bing-cha-ji-by-m-vjdr/ 一、基本概念 并查集是一种数据结构 并查集这…

并查集(详细解释+完整C语言代码)

目录 1概论 2.树的表现形式 3.代码实现 3.1创立集合 3.2合并 3.3查询 3.4路径压缩 第一个方法&#xff1a;查找时优化 第二个方法&#xff1a;合并时优化&#xff08;加权标记法&#xff09; 4.完整代码 4.1优化前 4.2优化后 1概论 并查集是一种十分精巧且实用的树形…

时间序列预测的一般步骤

一、ARIMA模型概述 ​​ ARMA模型就是AR和MA的简单结合&#xff0c;同时包含了历史数值项和错误项。由于AR和MA模型都对时间序列有平稳性要求&#xff0c;ARMA模型也存在这个限制&#xff0c;因此我们将其拓展到ARIMA模型&#xff0c;其可以解决非平稳性问题。引入的差分概念是…

时间序列预测算法总结

时间序列算法 time series data mining 主要包括decompose&#xff08;分析数据的各个成分&#xff0c;例如趋势&#xff0c;周期性&#xff09;&#xff0c;prediction&#xff08;预测未来的值&#xff09;&#xff0c;classification&#xff08;对有序数据序列的feature提…

基于深度学习的时间序列预测

# 技术黑板报 # 第十一期 推荐阅读时长&#xff1a;15min 前言 时间序列建模历来是学术和工业界的关键领域&#xff0c;比如用于气候建模、生物科学和医学等主题应用&#xff0c;零售业的商业决策和金融等。虽然传统的统计方法侧重于从领域专业知识层面提供参数模型&#xff0…

预测类问题中的时间序列预测模型

前言&#xff1a;时间序列预测模型适用于含有时间变量的模型预测&#xff0c;对中短期的预测有着较好的结果&#xff0c;对长期预测不适用。本文重点介绍ARIMA模型的原理及代码实现。 其他模型总结&#xff1a;​【Python】Python中的经典时间序列预测模型总结_风度78的博客-C…

Transformer 在时间序列预测中的应用

2017年&#xff0c;Google的一篇 Attention Is All You Need 为我们带来了Transformer&#xff0c;其在NLP领域的重大成功展示了它对时序数据的强大建模能力&#xff0c;自然有人想要把Transformer应用到时序数据预测上。在Transformer的基础上构建时序预测能力可以突破以往的诸…

【推荐收藏】11种比较常用的时间序列预测模型

时间序列及其预测是日常工作中建模&#xff0c;分析&#xff0c;预测的重要组成部分。 本文我们将从0开始介绍时间序列的含义&#xff0c;模型及其分析及其常用的预测模型。 文章目录 交流时间序列定义时间序列预测模型与方法原始数据朴素法简单平均法移动平均法指数平滑法一次…

时间序列预测的20个基本概念总结

1、时间序列 时间序列是一组按时间顺序排列的数据点 比如&#xff1a; 每小时的气压每年的医院急诊按分钟计算的股票价格 2、时间序列的组成部分 时间序列数据有三个主要组成部分。 趋势季节性残差或白噪声 3、趋势 在时间序列中记录的长期缓慢变化/方向。 4、季节性 …

时间序列-预测(Forcasting):时间序列预测算法总结

一、背景介绍 绝大部分行业场景,尤其是互联网、量化行业,每天都会产生大量的数据。金融领域股票价格随时间的走势;电商行业每日的销售额;旅游行业随着节假日周期变化的机票酒店价格等; 我们称这种不同时间收到的,描述一个或多种特征随着时间发生变化的数据,为时间序列…

时间序列预测 | ARMA应用指南

ARMA可谓是时间序列最为经典常用的预测方法&#xff0c;广泛应有于涉及时间序列的各个领域。ARMA模型自出道以来&#xff0c;出场次数不可胜数。想必大家也都不陌生&#xff0c;常学常新&#xff0c;我们今天不妨再来回顾一遍&#xff5e;。 ARMA全称Autoregressive moving ave…

时间序列预测的7种方法

import pandas as pd#取数 #dfpd.read_csv(jetrail.csv) #print(df.head()) ID Datetime Count 0 0 25-08-2012 00:00 8 1 1 25-08-2012 01:00 2 2 2 25-08-2012 02:00 6 3 3 25-08-2012 03:00 2 4 4 25-08-2012 04:00 2#pr…

Transformer时间序列预测

介绍&#xff1a; 提示&#xff1a;Transformer-decoder 总体介绍 本文将介绍一个 Transformer-decoder 架构&#xff0c;用于预测Woodsense提供的湿度时间序列数据集。该项目是先前项目的后续项目&#xff0c;该项目涉及在同一数据集上训练一个简单的 LSTM。人们认为 LSTM 在…

时间序列预测的8种常用方法简介

时间序列预测的7种方法 1. 朴素预测法&#xff08;Naive Forecast) 如果数据集在一段时间内都很稳定&#xff0c;我们想预测第二天的价格&#xff0c;可以取前面一天的价格&#xff0c;预测第二天的值。这种假设第一个预测点和上一个观察点相等的预测方法就叫朴素法&#xff…

常见的时间序列预测模型python实战汇总

最完整的时间序列分析和预测&#xff08;含实例及代码&#xff09;&#xff1a;https://mp.weixin.qq.com/s/D7v7tfSGnoAqJNvfqGpTQA 1 时间序列与时间序列分析 在生产和科学研究中&#xff0c;对某一个或者一组变量 x(t)x(t) ARIMA 模型对时间序列的要求是平稳型。因此&#…