文章目录
- [1. 概述](#1. 概述)
- [2. 原理](#2. 原理)
- [3. 实现](#3. 实现)
-
- [3.1 类结构设计](#3.1 类结构设计)
- [3.2 构造函数(初始化)析构函数(释放资源)](#3.2 构造函数(初始化)析构函数(释放资源))
- [3.3 查找对应元素的代表元](#3.3 查找对应元素的代表元)
- [3.4 判断两个元素是否处于同一个集合](#3.4 判断两个元素是否处于同一个集合)
- [3.5 合并两个元素](#3.5 合并两个元素)
- [3.6 查看整体数据中的集合数](#3.6 查看整体数据中的集合数)
- [3.7 测试代码](#3.7 测试代码)
- [3.8 路径压缩](#3.8 路径压缩)
- [4. 应用](#4. 应用)
1. 概述
故事引入:
话说在江湖中散落着各式各样的大侠,他们怀揣着各自的理想和信仰在江湖中奔波。或是追求武林至尊,或是远离红尘,或是居庙堂之高,或是处江湖之远。尽管大多数人都安分地在做自己,但总有些人会因为彼此的信仰不同而聚众斗殴。因此,江湖上常年乱作一团,纷纷扰扰。
这样长期的混战,难免会打错人,说不定一刀就把拥有和自己相同信仰的队友给杀了。这该如何是好呢?于是,那些有着相同信仰的人们便聚在一起,进而形成了各种各样的门派,比如我们所熟知的"华山派"、"峨嵋派"、",崆峒派"、"少林寺"、"明教"......这样一来,那些有着相同信仰的人们便聚在一起成为了朋友。以后再遇到要打架的事时,就不会打错人了。
但是新的问题又来了,原本互不相识的两个人如何辨别是否共属同一门派呢?
这好办!我们可以先在门派中选举一个"大哥"作为话事人(也就是掌门人,或称教主等)。这样一来,每当要打架的时候,决斗双方先自报家门,说出自己所在门派的教主名称,如果名称相同,就说明是自己人,就不必自相残杀了,否则才能进行决斗。于是,教主下令将整个门派划分为三六九等,使得整个门派内部形成一个严格的等级制度(即树形结构)。教主就是根节点,下面分别是二级、三级、......、N级队员。每个人只需要记住自己的上级名称,以后遇到需要辨别敌友的情况时,只需要一层层往上询问(网上询问)就能知道是否是同道中人了。
数据结构的角度来看:
由于我们的重点是在关注两个人是否连通,因此他们具体是如何连通的,内部结构是怎样的,甚至根节点是哪个(即教主是谁),都不重要。所以并查集在初始化时,教主可以随意选择(就不必再搞什么武林大会了),只要能分清敌友关系就行。
备注:上面所说的"教主"在教材中被称为"代表元"。即:用集合中的某个元素来代表这个集合,则该元素称为此集合的代表元。用树形结构的术语来说的话,就是这棵树的根
总结:在实际的应用场景中,我们经常会遇到一些问题,需要将n个不同的元素划分成一些不相交的集合,然后按照一定的规律将有一些相同共性的集合进行合并(并),需要一些操作快速的查找某些元素是否在同一个集合(查),为了能够高效的实现这些操作,设计出了一种数据结构叫做并查集(Union Find Set)
2. 原理
对于上述的需求,我们设计出了并查集这个数据结构,但是实际上可以给这个数据结构进行一些优化的设置
我们将使用数组作为底层结构来存放数据,假设一共有n个元素,那么就创建n个元素的数组,首先我们将所有下标对应的值设置为-1,表示他们单独成为一个集合(门派)对于属于同一集合的元素,在其对应位置存放他的上级的下标,以此类推
按照上面的场景,我们将有以下的需求:
- 判断两个元素是否处于同一集合
- 合并两个元素所在的集合
- 判断整体有多少个集合
- 查找对应元素的代表元
3. 实现
3.1 类结构设计
cpp
class UnionFindSet
{
private:
vector<int> _ufs; // 成员变量使用一个数组即可
public:
UnionFindSet(); // 构造函数
~UnionFindSet(); // 析构函数
bool IsSameSet(int x, int y); // 判断是否处于同一个集合
void Union(int x, int y); // 合并两个元素所在集合
size_t Count(); // 查看整体数据中的集合数
int FindRoot(int x); // 查找对应元素的代表元
};
3.2 构造函数(初始化)析构函数(释放资源)
构造函数可以根据实际情况更改或者重载,这里只实现根据整体的元素个数来构造
cpp
UnionFindSet(int n) :_ufs(n, -1) {}
// 析构函数,不需要析构,vector会自动调用他的析构函数
3.3 查找对应元素的代表元
我们知道对于属于同一集合的元素,在其对应位置存放他的上级的下标,代表元也就是说他本身没有上级(存放元素内容为-1),然后任何一个元素一直向上级查找,最终都能找到代表元
cpp
int FindRoot(int x) // 查找对应元素的代表元
{
int root = x;
while (_ufs[root] >= 0) // 循环查找,直到找到存放元素为负数的情况
{
root = _ufs[root]; // 找到当前位置的上级
}
return root;
}
3.4 判断两个元素是否处于同一个集合
如果两个元素拥有同一个代表元,那么就证明他们属于同一个集合
cpp
bool IsSameSet(int x, int y) // 判断两个元素是否处于同一个集合
{
int root1 = FindRoot(x); // 分别查找两个元素的代表元
int root2 = FindRoot(y);
return root1 == root2;
}
3.5 合并两个元素
要合并连个元素,不能够直接合并这两个元素,而是应该合并两个元素所在的两个集合,所以需要先找到对应集合的代表元,然后将其中一个集合的代表元设置为另一个集合的代表元的下级
cpp
void Union(int x, int y) // 合并两个元素所在集合
{
// 分别查找两个元素的代表元
int root1 = FindRoot(x);
int root2 = FindRoot(y);
if (root1 == root2) // 如果两个元素本来就属于同一个集合,就直接return
return;
// 这里假设root1为合并后的代表元,
// 1. 让root1存放的内容更改为整体
_ufs[root1] += _ufs[root2];
// 2. 更改root2内存放的值(root2现在是root1的下级)
_ufs[root2] = root1; // 所以要存放root1的下标
}
3.6 查看整体数据中的集合数
根据我们的设计,所有的代表元存放的值都是负数,所以集合的个数 == 代表元的个数 == 值为负数的元素个数
cpp
size_t Count() // 查看整体数据中的集合数
{
size_t count = 0;
for (auto& e : _ufs)
{
if (e < 0)
count++;
}
return count;
}
3.7 测试代码
cpp
// 这里是一个简单的测试,读者朋友们编写完代码后可以自行测试
void Test1()
{
UnionFindSet ufs(10);
cout << ufs.Count() << endl;
ufs.Union(0, 9);
ufs.Union(1, 8);
ufs.Union(2, 7);
ufs.Union(3, 6);
ufs.Union(4, 5);
ufs.Union(0, 1);
cout << ufs.Count() << endl;
cout << ufs.FindRoot(9) << endl;
cout << ufs.FindRoot(8) << endl;
cout << ufs.IsSameSet(8, 9) << endl;
cout << ufs.IsSameSet(5, 9) << endl;
}
3.8 路径压缩
在一些对效率要求比较高的地方,我们可能会采取一些优化的方式,让查找的效率变得更高。经过分析可以发现,查找代表元的过程效率跟下级的个数(树的层数)有关,但是我们不关心同一个集合的上下级关系,所以可以让集合内除代表元之外的所有元素都是代表元的直接下级,这样查找的效率就变成了O(1),可是每一次都这样更新是比较复杂并且消耗时间资源的,所以没有必要单独更新,可以把更新的行为和查找放在一起
只要查找一次,就将查找路径上的所有结点都挂到根结点下面,如图,查找L的根结点A,查找一次过后,就将E、B、L全部挂到根结点A之下
cpp
int Find1(int x) // 路径压缩1
{
int root = x;
while (_ufs[root] >= 0) // 循环查找,直到找到存放元素为负数的情况
{
root = _ufs[root];
}
while (x != root) //x不为根结点,则压缩路径
{
int t = _ufs[x]; //t指向x的父节点
_ufs[x] = root; //x直接挂到根结点下
x = t;
}
return root;
}
扩展:加权标记法优化
上面代码汇总
cpp
#include <iostream>
#include <vector>
using namespace std;
class UnionFindSet
{
private:
vector<int> _ufs; // 成员变量使用一个数组即可
public:
UnionFindSet(int n) :_ufs(n, -1) {}
// 析构函数,不需要析构,vector会自动调用他的析构函数
bool IsSameSet(int x, int y) // 判断两个元素是否处于同一个集合
{
int root1 = FindRoot(x); // 分别查找两个元素的代表元
int root2 = FindRoot(y);
return root1 == root2;
}
void Union(int x, int y) // 合并两个元素所在集合
{
// 分别查找两个元素的代表元
int root1 = FindRoot(x);
int root2 = FindRoot(y);
if (root1 == root2) // 如果两个元素本来就属于同一个集合,就直接return
return;
// 这里假设root1为合并后的代表元,
// 1. 让root1存放的内容更改为整体
_ufs[root1] += _ufs[root2];
// 2. 更改root2内存放的值(root2现在是root1的下级)
_ufs[root2] = root1; // 所以要存放root1的下标
}
size_t Count() // 查看整体数据中的集合数
{
size_t count = 0;
for (auto& e : _ufs)
{
if (e < 0)
count++;
}
return count;
}
int FindRoot(int x) // 查找对应元素的代表元
{
int root = x;
while (_ufs[root] >= 0) // 循环查找,直到找到存放元素为负数的情况
{
root = _ufs[root]; // 找到当前位置的上级
}
return root;
}
int Find1(int x) // 路径压缩1
{
int root = x;
while (_ufs[root] >= 0) // 循环查找,直到找到存放元素为负数的情况
{
root = _ufs[root];
}
while (x != root) //x不为根结点,则压缩路径
{
int t = _ufs[x]; //t指向x的父节点
_ufs[x] = root; //x直接挂到根结点下
x = t;
}
return root;
}
};
void Test1()
{
UnionFindSet ufs(10);
cout << ufs.Count() << endl;
ufs.Union(0, 9);
ufs.Union(1, 8);
ufs.Union(2, 7);
ufs.Union(3, 6);
ufs.Union(4, 5);
ufs.Union(0, 1);
cout << ufs.Count() << endl;
cout << ufs.FindRoot(9) << endl;
cout << ufs.FindRoot(8) << endl;
cout << ufs.IsSameSet(8, 9) << endl;
cout << ufs.IsSameSet(5, 9) << endl;
}
int main()
{
Test1();
return 0;
}
4. 应用
下面是两个leetcode的题目,感兴趣的同学可以做一做,练习一下并查集的相关操作
本节完...