引言
并查集(Union-Find)是一种高效的数据结构,主要的操作有:
- 合并(Union)
- 查找(Find)
- 路径压缩(可选)
其中最基本的便是合并与查找
合并
为方便叙述,把所有元素视作点,元素之间的关系视作线,存在联系便存在关系(需要注意的是,这里的关系应当是1.自反的,2.对称的,3.传递的)
- 自反:x与x存在关系
- 对称:若x与y存在关系,则y与x也存在关系
- 传递:若x与y存在关系,y与z存在关系,那么x与z也存在关系
所谓合并,便是将两个点之间“画”一条线。 又上边的定义不难理解相连的若干点之间互相存在关系,这样我们便可以吧相连的若干点看做一个**“等价类”或“连通分支”**
合并主要的流程为:
- 查找两个点的根节点(参见后边查找部分)
- 判断两个点是否存在关系,即根节点是否相同
- 若根节点相同,什么都不做;否则,合并两个点:将两个点所在的连通分支合并为一个连通分支,即将连通分支的根节点的前驱节点改为另一个连通分支的根节点
- (上一步的优化版)若根节点相同,什么都不做;否则,合并两个点:将节点数较少的连通分支合并到节点数较多的连通分支上
查找
我们采用线性的数据结构:数组,来表示并查集:
count
表示连通分支的个数,初始化为节点个数N- 数组
front[]
用来存储N个点的前驱节点,初始化为节点自己 - 数组
size[]
用来表示连通分支包含的节点数,初始化为1(初始时每个节点都视为一个连通分支) - 每次合并
count--
路径压缩
路径压缩即为,将所有节点的前驱节点改为连通分支的根节点
这样在寻找节点的根节点时耗时更少。
总结
使用路径压缩,和优化后的合并方法的并查集算法已经是理论上的最优算法。
附上LeetCode547题的题解:
class Solution {
public int findCircleNum(int[][] M) {
int N = M.length; // 元素数量
UF uf = new UF(N);
// 遍历关系矩阵,如果i,j没有连接,连接他们
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (M[i][j] == 1) uf.union(i, j);
}
}
// 得到朋友圈数量
int count = uf.count();
return count;
}
}
// Union-Find Class
class UF {
private int[] front; // 存储前驱节点的位置
private int[] size; // 存储连通分支的大小
private int count; // 连通分支的个数
public UF(int N) { // 初始化
front = new int[N];
size = new int[N];
count = N;
for (int i = 0; i < N; i++)
front[i] = i; // 指向自己
for (int sz : size)
sz = 1; // 将每个节点视为一个连通分支
}
// 查找点p的根节点
public int find(int p) {
int temp = p;
while(p != front[p]) p = front[p];
// 路径压缩
front[temp] = p;
return p;
}
// 判断p,q是否连接
public boolean connected(int p, int q) {
return find(p) == find(q);
}
// 得到连通分支数
public int count() {
return count;
}
// 合并连通分支
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) return;
else { // 优化版的合并
if (size[pRoot] > size[qRoot]) {
front[qRoot] = pRoot;
} else {
front[pRoot] = qRoot;
}
}
// 合并后连通分支数减一
count--;
}
}