并查集
前言
在现实中,我们可能会遇到判断一个物品属于哪个集合的情况。例如,我们看到一个人,只需要一眼我们就能知道他是属于人这个物种集合里的生物。
那么在计算机里如何实现这个操作的呢?
我们很容易就能够想到,设置一个表示集合的结点,然后给每个元素添加一个指针,指向从属的集合结点。
现在又有一个问题了:我们现在有苹果集合、梨子集合、香蕉集合,它们都是属于水果集合,那么在计算机里该怎么表示这个集合关系呢?
很显然,我们只需要让代表苹果集合、梨子集合的结点、香蕉集合的结点指向代表水果集合的结点就可以了。
定义
读了前言的内容,有些同学肯定能够想到,这种方式不是树的双亲表示法吗?
确实如此!
在数据结构中,我们将类似这种方法表示的集合称为并查集,它适用于操作不相交的集合,可以快速查找一个元素所属的集合以及将一个集合与另一个集合合并。
并查集的实现
这里我们用数组实现一个简单的并查集。
定义
const MAX_SIZE = 100;
int UnionFindSet[MAX_SIZE];
初始化操作
/**
* 初始化操作
*
* 将每个元素分割为单独的集合
*
* @param {int[]} s
*/
void initial(int s[]){
for(auto i = 0; i < MAX_SIZE; i++)
s[i] = -1;
}
查找操作
/**
* 查找一个元素所属的集合
*
* 结合图示,只需要沿着指针所代表的结点一直查找下去即可。
*
* 时间复杂度为O(h),h为树的高度。
*
* @param {int[]} s 并查集
* @param {int} x 待查找元素位序
*
* @return {int} 元素所属集合
*/
int find(int s[], int x){
while(s[x] >= 0) x = s[x];
return x;
}
合并操作
/**
* 合并两个集合
*
* 将两个集合合并为一个集合,只需要让一个集合的根指向另一个元素的根即可。
* 例如,苹果集合并入水果集合,只需要将苹果集合的根指向水果集合的根。
*
* 时间复杂度为 O(1)
*
* @param {int[]} s 并查集
* @param {int} root1 集合1的根
* @param {int} root2 集合2的根
*/
void union(int s[], int root1, int root2){
if(root1 == root2) return;
else (s[root2] = root1);
}
并查集的优化
在上述的查找操作中,查找操作的时间复杂度和树的高度有关。在极端情况下,在含有n个元素的集合进行find操作的时间复杂度是O(n),所有我们需要尽可能地降低树的高度。
我们可以通过每次将元素数较小的集合并入到元素数较大的集合,来降低深度。这种方式构造的并查集,其深度不超过 ⌊ log 2 n ⌋ + 1 \lfloor\log_2 n\rfloor + 1 ⌊log2n⌋+1
如果不需要维护原有的从属关系,那么我们可以在查找的时候,让路径上的结点直接指向根节点。(例如,如果要维护从属关系,那么苹果属于苹果集合,苹果集合属于水果集合,所以苹果才属于水果集合,但是如果不维护的话,那么苹果可以不属于苹果集合,而是直接属于水果集合。)
优化这部分只提供思路,如果有感兴趣的同学,可以看一看这篇文章https://oi-wiki.org/ds/dsu/
全篇至此结束,感谢阅读。