并查集(union-find)是一种十分简洁而优雅的数据结构之一,主要用于解决一些元素分组的问题。它管理一系列不相交的集合,并支持两种操作:
- 合并(Union):把两个不相交的集合合并为一个集合。
- 查询(Find):查询两个元素是否在同一个集合中。
并查集的逻辑结构
如有一组数据 { 1,2,3,4,5,6,7,8 }
,其中,{ 1,2,3 }
,{ 4,5 }
,{ 6,7,8 }
各自组成独立的集合,即集合相交为空,我们如何判断两个元素是否在同一集合中呢?
一种简单的想法是,我们给每个元素打上一个标签,如 { 1,2,3 }
中的元素标签为 a
,{ 4,5 }
中的元素标签为 b
,这样,在判断两个元素是否属于同一集合时,只需判断两元素的标签是否一致。但是这样有一个问题,如果我想将两个集合合并(union),如{ 1,2,3 }
和{ 6,7,8 }
合并成为{ 1,2,3,6,7,8 }
,这样我们就需要更改第一个或第二个集合中的全部元素的标签,如果集合中的元素非常多,这样做的代价将是很大的。
为此,我们需要对这个想法做一些改进。
首先,对于标签的选取,我们应该注意到,由于集合两两相交为空,所以我们可以在每个集合中选取一个元素作为这个集合中元素的标签,这样就能确保不同集合的元素标签不同,如下图所示:
接下来,我们就要解决两个集合合并的问题。
从上图可以看出,这是一种类似于树的结构,我们可以通过子节点找到他的父节点,既然如此,我们可以让树的根节点作为这棵树的唯一标识,这样就可以增加树的高度,当判断两个元素是否处于同一个集合时,我们只需判断两个元素是否处于同一棵树,即只需判断他们的所在树的根节点是否相同即可。
我们可以用类似于合并两棵树的操作合并两个并查集:
看似完美了,但是注意,和树一样,这样的操作有可能使并查集的结构退化成一个链表:
为了防止这种情况的,我们需要做两件事:
- 合并两个并查集时,将高度更大的树的根节点作为合并后的根节点,这样可以保证合并后树的高度最小
- 适时将节点上移。如在进行
find
操作时,将带查询节点的父直接更改为根节点。
并查集的实现
具体实现时,不必实现一颗具体的树,只需要用数组或者map来模拟树的情况,具体实现要根据需求变化。
public class UF {
// 存储元素 i 的父节点
private int[] parent;
// 以节点 i 为根的树的深度
private byte[] rank;
// 独立集合的个数
private int count;
/**
* 初始化一个空的并查集
* @param n the number of elements
* @throws IllegalArgumentException if n < 0
*/
public UF(int n) {
if (n < 0) {
throw new IllegalArgumentException();
}
count = n;
parent = new int[n];
rank = new byte[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 0;
}
}
/**
* 返回包含元素p的并查集的根节点
* @param p an element
* @return the canonical element of the set containing p
* @throws IllegalArgumentException unless 0 <= p < n
*/
public int find(int p) {
validate(p);
while (p != parent[p]) {
// path compression by halving
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
/**
* Returns the number of sets.
* @return the number of sets between 1 and n
*/
public int count() {
return count;
}
// 检查p、q是否属于同一个并查集
public boolean connected(int p, int q) {
return find(p) == find(q);
}
/**
* 合并包含p、q的两个并查集
* @param p one element
* @param q the other element
* @throws IllegalArgumentException unless
* both {@code 0 <= p < n} and {@code 0 <= q < n}
*/
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) {
return;
}
// make root of smaller rank point to root of larger rank
if (rank[rootP] < rank[rootQ]) {
parent[rootP] = rootQ;
} else if (rank[rootP] > rank[rootQ]) {
parent[rootQ] = rootP;
} else {
parent[rootQ] = rootP;
rank[rootP]++;
}
count--;
}
// 验证p是否存在
private void validate(int p) {
int n = parent.length;
if (p < 0 || p >= n) {
throw new IllegalArgumentException("index " + p + " is not between 0 and " + (n-1));
}
}
}