并查集是一种不一样的树形结构,它主要的作用是解决连接问题。比如判断下图中的两个节点是否相连,如果是相邻的两个点那么可以很快判断出来,但是对于相隔较远的点就需要使用并查集来判断了。在现实生活中我们主要使用连接问题判断网络之间的连接关系,这种网络包括计算机网络以及社交网络等等。
实现
在并查集中我们需要实现的功能主要就是
- 实现两个元素合并为同一个集合下 u n i o n ( p , q ) union(p,q) union(p,q)
- 查看某个元素属于哪一个集合 f i n d ( p ) find(p) find(p)
- 判读两个元素是否连接 i s C o n n e c t e d ( p , q ) isConnected(p,q) isConnected(p,q)
对于连接问题,我们首先将元素编号,那么连接 问题中的所有点就可以用一个数组来表示,通过对数组值的修改就可以实现并查集了。在执行union操作时,如果将p 和 q 连接,那么相当于是将p的集合和q的集合进行了合并,所以p集合中的所有ID都需要改为q集合的ID。
#include <iostream>
class UnionFind{
private:
int* id;
int count;
public:
UnionFind(int n){
id = new int[n];
count = n;
for(int i=0; i<n; i++)
id[i] = i; //每个元素属于集合i,即每个元素所属集合不同
}
~UnionFind() { delete[] id; }
int find(int p){
return id[p];
}
bool isConnected(int p , int q){
return find(p) == find(q);
}
void unionElement(int p , int q){
int pId = id[p];
int qId = id[q];
if(pId == qId)
return;
for(int i=0; i<n; i++){
if(id[i] == pId)
id[i] = qId;
}
}
}
优化
在上面的实现过程中,因为需要对每个元素都进行遍历,所以每次union的复杂度为
O
(
n
)
O(n)
O(n),如果对于
n
n
n个数据都进行union操作,那么复杂度就是
O
(
n
2
)
O(n^2)
O(n2)了。而下面采用的Quick Find的思路可以对这种情况进行一定的优化。
具体的思想是将每个元素看做一个节点,如果两个节点是连接状态,就将其中一个节点的指针指向另一个节点。而在初始状态,每个节点的指针都指向自己。如图所示,表示1,2,3的连接状态,可以将1,3的指针分别指向2,同时保证2的指针指向自己,这样就能保证一个完整的连接。
因为这里的指针只需要指向父亲节点,所以仍然可以使用数组进行实现。假设每个元素初始父亲节点指向自己。
此时如果需要union(4,3),只需要将4的元素修改为3即可。
那么这里的实现思路就简单了,每次进行union操作时,只需要找到两个节点的根节点,然后将一个根节点指向另一个根节点就可以了。
#include <iostream>
class UnionFind{
private:
int* parent;
int count;
public:
UnionFind(int n){
parent = new int[n];
count = n;
for(int i=0; i<n; i++)
parent[i] = i;
}
~UnionFind() { delete[] parent; }
int find(int p){
while(p != parent[p]){
p = parent[p];
}
return p;
}
bool isConnected(int p , int q){
return find(p) == find(q);
}
void unionElement(int p , int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
parent[pRoot] = qRoot;
}
}
基于size的优化
但是在上述过程中,如果在合并过程中,将数量大的连接到数量小的上就会导致整个集合的高度提高,那么在find操作中需要向上寻找的次数就会增多,导致遍历时间增加。那么这里可以使用size的优化,将size数量较小的集合连接到数量较大的集合上。
实现方式也很简单只需要添加一个size的数组,每次union操作对size进行维护就可以了。
#include <iostream>
class UnionFind{
private:
int* parent;
int count;
int* size; //size[i]表示以i为根的集合元素
public:
UnionFind(int n){
parent = new int[n];
size = new int[n];
count = n;
for(int i=0; i<n; i++){
parent[i] = i;
size[i] = 1;
}
}
~UnionFind() {
delete[] parent;
delete[] size;
}
int find(int p){
while(p != parent[p]){
p = parent[p];
}
return p;
}
bool isConnected(int p , int q){
return find(p) == find(q);
}
void unionElement(int p , int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
if(size[pRoot] < size[qRoot]){
parent(pRoot) = qRoot;
size(qRoot) + = size(pRoot);
}
else{
parent(qRoot) = pRoot;
size(pRoot) + = size(qRoot);
}
}
}
基于rank的优化
但是还有可能存在一种极端情况,假如集合A节点较少,但是层数比较高,而B集合节点多,但是层数少,若将A集合连接到B上,则会导致总体的层数增加,所以此时最合适的应该是基于高度判断哪一个集合作为根。
实现方法同上,只是需要对rank数组进行维护。
#include <iostream>
class UnionFind{
private:
int* parent;
int count;
int* rank; //size[i]表示以i为根的集合元素
public:
UnionFind(int n){
parent = new int[n];
rank = new int[n];
count = n;
for(int i=0; i<n; i++){
parent[i] = i;
rank[i] = 1;
}
}
~UnionFind() {
delete[] parent;
delete[] rank;
}
int find(int p){
while(p != parent[p]){
p = parent[p];
}
return p;
}
bool isConnected(int p , int q){
return find(p) == find(q);
}
void unionElement(int p , int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
if(rank[pRoot] < rank[qRoot]){
parent(pRoot) = qRoot;
}
else if(rank[pRoot] > rank[qRoot]{
parent(qRoot) = pRoot;
}
else{
parent(pRoot) = qRoot;
rank[qRoot] + = 1;
}
}
}
路径压缩
上述操作都是针对union的,但是对于find并没有采取优化方式。对于下图这种一长串的集合,find操作时比较耗时的。此时可以在寻找时每次向上2个节点寻找,如果**没有到根节点的话,将当前节点的父节点赋值为父节点的父节点。**这样在find的过程中就将整个集合的高度压缩了。
因为只对find操作进行修正所以只需要修改find函数即可。
int find(int p){
while(p != parent[p]){
parent[p] = parent[parent[p]];
p = parent[p];
}
return parent[p];
}
路径压缩虽然对rank的值有影响,但是在实践中其实并不影响。因为在find的过程中,每个集合的高度都发生了减小,所以rank的相对大小依然保持恒定,并不影响总体性能。