概述
并查集, 书面叫不相交集. 由于关于这个数据结构(集合)主要操作是Union(集合求并)和Find(查找一个元素属于哪个集合), 所以经常见到叫并查集.
问题引入
为了对这种数据结构有个感性的认识, 先给一个实际的问题:
有10台电脑, 任意两台电脑可能有网线连接, 此时说这两个电脑连通. 我们认为A和B连通, B和C连通时, 那么A和C也连通.
问题是: 现在给定一些电脑的连通情况, 求指定的两台电脑是否连通.
问题解决思路: 将10台电脑分成10个集合, 初始每个集合中只有一台电脑.
在给定两台电脑(如A和B)连通时, 将A, B所在集合合并, 依次这样操作.
最后问指定的两台电脑是否连通时, 只要看它们是不是在一个集合中就可以了.
上面的思路就是并查集的想法.
下面回到代码, 用代码如何表示上述结构?
代码演示
给定N
个元素时, 我们将其编号0
到N-1
, 开一个数组arr[N]
,
此时数组索引就对应一个元素, arr[i]
表示元素i
的父节点(一个集合对应着一棵树).
如果i
为树根, 令arr[i]
为负数以示区分.
此时Union操作, 就是改变一棵树的根的父节点, Find操作就是找父节点为负数的节点.
基本的数据结构定义
#define N 10 // 所有集合元素总数
typedef int DisjointSet[N]; // 所有集合元素
typedef int SetType;
typedef int ElementType;
初始化操作
初始化操作就是将每个元素单独做一个集合, 此时单个元素都在树根上.
void Initialize(DisjointSet s) {
for (int i = 0; i < N; i++) {
s[i] = -1; // 所有元素自成一个集合(树), 初始在树根的位置
}
}
Find
查找操作就是给定一个元素, 找到它所属的集合(树根)
思路很简单, 当s[x]为负数时, 表示x就是树根, 否则令x=s[x]继续住上找.
// 查找元素x所在的集合(返回树根)
SetType Find(ElementType x, DisjointSet s) {
if (s[x] < 0) { // s[x]负数, 说明x在树根的位置
return x;
}
#if (FIND_TYPE == 1)
return Find(s[x], s); // 直接递归
#elif (FIND_TYPE == 2)
return s[x] = Find(s[x], s); // 这叫"路径压缩", 其实就是尾递归
#elif (FIND_TYPE == 3)
while (s[x] >= 0) { // 自己写的迭代实现
x = s[x];
}
return x;
#endif
}
Union
并操作, 相当于两棵树根要连接上, 有很多方法. 比如将下边两棵树合并:
其实Union要关心的是以谁的根为新根的问题. 为什么要关心这个呢? 观察上述的第1种合并,
明显新树高度变高了, 这样不利于查找(注意理解Find函数的工作过程).
基于此, 给出三种方法, 分别是:
- 一直以第一棵树根作为新树的根 (随机合并)
- 以元素个数多的树根作为新树的根 (按规模合并)
- 以高度(也叫秩, rank)高的根根作为新树的根 (按秩合并)
需要注意的是, 对于上述第2种和第3种合并方法, s[root], 即根的父节点存储的是规模或者高度的负值.
// 两个集合作并集, 合并后root1为新树树根
void Union(DisjointSet s, SetType root1, SetType root2) {
int tmp;
#if (UNION_TYPE == 1)
s[root2] = root1; // root2所在树的根现在是root1
#elif (UNION_TYPE == 2)
// s[root]表示s[-N], N为集合元素的个数
// 所以s[root2] > s[root1], 表示集合2比集合1"规模小"
// 所以树2要贴到树1上
if (s[root2] > s[root1]) {
tmp = s[root2];
s[root2] = root1;
s[root1] += tmp; // 更新下集合1的规模
} else {
tmp = s[root1];
s[root1] = root2;
s[root2] += tmp;
}
#elif (UNION_TYPE == 3)
if (s[root2] < s[root1]) { // 树2比树1高
s[root1] = root2; // 树1贴到树2上
} else {
// 两树等高, 选择树2贴到树1上
// 树1高度加1
if (s[root1] == s[root2]) {
s[root1]--;
}
s[root2] = root1;
}
#endif
}
Connected
当然还有一个不太标准的操作, 判断两个元素是否属于同一个集合(是否连通)
// 判断两个元素是否属于同一个集合(属于同一棵树)
bool Connected(ElementType x, ElementType y, DisjointSet s) {
return Find(x, s) == Find(y, s);
}
利用上述的成果, 对开始的关于"电脑连通"的问题给出解答:
给电脑编号0到9, 测试数据testData表示已知的连通情况, 求解电脑0和9的连通情况.
int main(int argc, char *argv[])
{
// {1, 2} 表示1, 2两个电脑连通
int testData[][2] = {
{0, 1},
{1, 2},
{2, 4},
{4, 5},
{5, 6},
{6, 8},
{8, 9}
};
DisjointSet s;
Initialize(s);
for (int i = 0; i < sizeof(testData) / sizeof(testData[0]); i++) {
Union(s, Find(testData[i][0], s), Find(testData[i][1], s));
}
if (Connected(0, 9, s)) {
printf("connected\n");
} else {
printf("not connected\n");
}
return 0;
}
完整代码参见这里
参考
数据结构与算法分析: C语言描述(第2版) 第8章 不相交集ADT
欢迎补充指正!