并查集
并查集是一种简单而高效的数据结构,主要用于处理一些分离的元素集合的合并与查询问题。它在计算机科学中应用广泛,尤其适用于那些需要动态管理和查询元素分组情况的场景,如网络连接、图的连通分量、社交网络中的群组划分等。通过并查集,我们可以快速判断两个元素是否属于同一个集合,以及将两个不同的集合合并为一个。其高效的操作使得并查集成为处理大型数据集中群组、连通性等问题的首选数据结构。
1.并查集操作
-
查找(Find):确定特定元素属于哪个子集。此操作可帮助确定两个元素是否属于同一子集。
-
合并(Union 或 Merge):将两个子集合并成一个单独的子集。
2.实现方法
方法1:快速查找
快速查找的思想是利用一个数组来跟踪每个元素所属的集合。在这种方法中,数组的每个位置对应一个元素,数组中存储的值代表该元素所属的集合标识符。这里的“集合标识符”通常是该集合中某个元素的标识,通常是这个集合中最小的元素,但实际上可以是任何标识。(假设有一个名为
Set
的数组,其大小是N
(元素的数量)。初始时,每个元素都属于只包含其自身的集合,因此Set[i] = i
对于所有0 <= i < N
。)
i.查找操作 (Find)
查找操作非常简单且直接:给定元素x
,函数find1
只需返回数组中x
位置上的值。这个值就是x
所属的集合的标识符。因为直接通过索引访问数组,所以这个操作的时间复杂度是O(1)。
// 查找操作
int find1(int x) {return Set[x];
}
ii.合并操作 (Union or Merge)
合并操作涉及到将两个集合合并为一个。这需要将这两个集合中的所有元素更新为拥有相同的集合标识符。在快速查找策略中,这通常涉及到遍历整个数组,将所有属于这两个集合之一的元素的集合标识符更新为统一的值(通常是两个集合标识符中的最小值)。因为这涉及到遍历整个数组,所以时间复杂度为O(N)。
// 合并操作
void merge1(int a, int b) {int setA = find1(a); // 找到a的集合标识符int setB = find1(b); // 找到b的集合标识符if (setA != setB) { // 如果a和b不在同一集合,则合并for (int i = 0; i < N; ++i) {if (Set[i] == setB) { // 将所有属于集合B的元素的集合标识符更新为A的集合标识符Set[i] = setA;}}}
}
方法2:快速合并
快速合并优化了合并集合的操作,但是这使得查找操作可能变慢。在快速合并的实现中,每个集合都由一个树来表示,集合中的每个元素都是树的一个节点。每个节点都指向另一个节点(除了根节点,它指向自己),这个指向表示了元素之间的关联。集合的“根”节点是该集合的代表。
i.查找操作 (Find)
查找操作find2
需要找到元素所在集合的根节点,因为根节点是该集合的代表。开始时,find2
函数从指定的节点开始,沿着指向父节点的指针向上遍历,直到找到一个指向自己的节点,即根节点。这个过程可能需要遍历整个树的高度,所以在最坏的情况下时间复杂度为O(N)。
int find(int x) {while (parent[x] != x) { // 当x不是自己的父亲(即不是根元素)时parent[x] = parent[parent[x]]; // 进行路径压缩,将x的父亲设置为x的爷爷节点x = parent[x]; // x上移至其父亲节点}return x; // 返回x的根元素,即集合的标识
}
ii.合并操作 (Union or Merge)
合并操作merge2
简单到只需要将一个集合的根节点指向另一个集合的根节点。因为这只需要更改一个节点的父指针,所以这个操作的时间复杂度是O(1)。
// 合并操作
void merge2(int a, int b) {int rootA = find2(a); // 找到a的根int rootB = find2(b); // 找到b的根if (rootA != rootB) { // 如果a和b不在同一集合,将一个集合的根指向另一个集合的根Set[rootA] = rootB;}
}
iii.路径压缩优化
为了改善查找操作的效率,可以应用路径压缩技术。当执行查找操作以找到一个元素的根节点时,路径压缩将所有遍历过的节点直接连接到根节点上,这样它们在下一次查找时将直接指向根节点,大大减少了遍历的路径长度。通过这种方式,查找操作的平均时间复杂度可以显著减少。
// 查找操作,带路径压缩
int find2(int x) {if (Set[x] != x) {Set[x] = find2(Set[x]); // 路径压缩}return Set[x];
}
iv.基于秩的合并
基于秩的合并是一种优化并查集性能的策略,尤其是在
merge
和find
操作上。在这种策略中,我们通过保持树的高度尽可能低来提高效率。这里的“秩”可以理解为树的高度或深度的一个估计值,并不一定是实际的高度。
每个元素都有一个秩,初始时秩都设置为0。当两个集合合并时,如果它们的秩相同,则选择一个作为新的根,并将其秩增加1;如果它们的秩不同,则将秩较小的树合并到秩较大的树下,而不改变秩较大树的秩。这样做可以避免树变得太高,保证了find
操作的效率。
class UnionFind {vector<int> parent; // 存储每个元素的父节点vector<int> rank; // 存储每个根节点的秩public:UnionFind(int size) : parent(size), rank(size, 0) {// 初始时,每个元素的父节点是它们自己for (int i = 0; i < size; ++i) {parent[i] = i;}}// 查找元素x所在集合的根节点int find(int x) {if (parent[x] != x) {// 路径压缩parent[x] = find(parent[x]);}return parent[x];}// 合并元素x和元素y所在的集合void merge(int x, int y) {int rootX = find(x);int rootY = find(y);if (rootX != rootY) {// 将秩较小的树的根连接到秩较大的树的根if (rank[rootX] < rank[rootY]) {parent[rootX] = rootY;} else if (rank[rootX] > rank[rootY]) {parent[rootY] = rootX;} else {// 如果秩相等,则任选一个作为根,并增加其秩parent[rootY] = rootX;rank[rootX] += 1;}}}
};