- 定义
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。
并查集是一种特殊的树结构,在其他的树结构中都是由父节点指向孩子节点,而在并查集中是由孩子节点指向父亲节点,并查集主要是用来解决连接问题的。
对于一组数据,并查集支持两个基本的动作:isConnected(p, q)
p 和 q 是否属于同一个集合;union(p, q)
将 p 和 q 所在的集合合并起来。由于可以使用不同的方式实现并查集,所以我们可以定义以下接口。
public interface UnionFindSet{
//将 p 位置对应数据的所在集合和 q 位置对应的数据的所在集合合并起来
public void union(int p, int q);
// p 位置的数据和 q 位置的数据是否属于同一个集合
public boolean isConnected(int p, int q);
// 数据 q 对应的集合编号
public int find(int q);
int getSize();
}
并查集的基本数据表示可以使用数组实现,我们可以对并查集中的数据进行编号,比如使用数组的索引表示对应的数据编号,数组中的值表示该数据对应的集合编号。如在下列中,分别使用索引 0 - 9 表示并查集中的数据,而 0、2、4、6、8 在 0 这个集合中,而其他数据在另一个集合 1 中。
索引: 0 1 2 3 4 5 6 7 8 9
数据: 0 1 0 1 0 1 0 1 0 1
- 基本使用
在并查集的初始化时,应该指定每个数据都在不同的集合中,如下,此时数组的索引和对应的值相等,表示每个数据都在不同的集合中。
private int[] data;
//把每个数据所在集合初始化为其自身, 也就是每个数据都在不同的集合中
public UnionFind(int size){
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = i;
}
}
(1)Quick Find
我们首先来看并查集的第一个实现,其主要思想是:当连接两个数据q, p时,通过遍历数组将q的集合编号修改为 p 的集合编号,具体实现如下:
/*
* 0 1 2 3 4 5 ==> 0 1 2 3 4 5
* 0 1 0 1 0 1 0 0 0 0 0 0
* 通过遍历数组,将 数组中 p 的集合编号全部修改为 q 的集合编号的值 ,时间复杂度为O(n)
* */
@Override
public void union(int p, int q) {
int qListNo = find(p);
int pListNo = find(p);
if(pListNo == qListNo) return;
for (int i = 0; i < data.length; i++) {
if(qListNo == data[i]) data[i] = pListNo;
}
}
// 在这种情况下,判断两个数据是否在同一个集合,只需要判断对一个的值是否相等即可,O(1)级别,所以叫 Quick Find
@Override
public boolean isConnected(int p, int q) {
return find(q) == find(p);
}
@Override
public int find(int q) {
if(q < 0 || q >= data.length) throw new IllegalArgumentException("参数无效");
return data[q];
}
(2)Quick Union
在标准情况下,并查集的实现思路是第二种,也就是Quick Union,其主要思想为:将每个元素看做一个节点(数组索引),节点的值(数组中的值)就表示当前节点指向的节点(索引),当前的节点和所指的节点所处通过一个集合中。当节点指向自己时,表示当前节点为根节点,连接两个数据时,只需要将其中一个数据的根节点指向另个数据(节点)的根节点即可;判断两个数据是否处于同一个集合中,只需要判断两个数据的根节点是否相等即可。
// 将 p 的根节点 指向 q 的根节点
@Override
public void union(int p, int q) {
int qRootIndex = find(q);
int pRootIndex = find(p);
if(qRootIndex == pRootIndex) return;
data[qRootIndex] = pRootIndex;
}
// 如果 p, q的根节点相同,表示处于同一个集合中
@Override
public boolean isConnected(int p, int q) {
return find(q) == find(p);
}
// 寻找 q 对应的根节点 对应的位置
@Override
public int find(int q) {
while(data[q] != q) q = data[q];
return q;
}
(3)基于 size 的优化
在第二个实现的连接方法中,我们并没有对根节点之间的连接做判断,这样可能会导致形成的树的高度过高(甚至变为链表),导致合并或者查询性能变低,解决方案就是给每个树维持一个节点个数size
,该值表示当前节点为根的树所包含的节点个数。在合并时,让size
小的根节点指向大的根节点。具体实现如下:
private int[] data;
private int[] sz; //sz[i] 表示以 i节点 为根的树所包含的元素个数
//把每个数据所在集合初始化为其自身, 也就是每个数据都在不同的集合中
public UnionFindThree(int size){
data = new int[size];
sz = new int[size];
for (int i = 0; i < size; i++) {
data[i] = i;
sz[i] = 1;
}
}
// 将 p 的根节点 指向 q 的根节点
@Override
public void union(int p, int q) {
// 分别获得q, p 的根节点的索引
int qRootIndex = find(q);
int pRootIndex = find(p);
if(qRootIndex == pRootIndex) return;
//如果q 高, 让 p 指向 q
if(sz[qRootIndex] >= sz[pRootIndex]) {
data[pRootIndex] = qRootIndex;
sz[qRootIndex] += sz[pRootIndex];
}else{
data[pRootIndex] = pRootIndex;
sz[pRootIndex] += sz[qRootIndex];
}
}
(4)基于 Rank 的优化
在基于 size
的优化时,我们假设size
较小的树高度可能会更小,但实际上不是这样的,比如下列这种情况,当连接【1,9】时,如果使用第三种优化,那么树的整体高度会变高,性能会降低。所以基于 Rank 的优化和第三种方式类似,只不过不同的是它是给每一个节点维持一个高度,让高度低根节点指向高度高的根节点。
private int[] height; //height[i] 表示以 i节点 为根的树的高度
//把每个数据所在集合初始化为其自身, 也就是每个数据都在不同的集合中
public UnionFindFour(int size){
data = new int[size];
height = new int[size];
for (int i = 0; i < size; i++) {
data[i] = i;
height[i] = 1;
}
}
// 将 p 的根节点 指向 q 的根节点
// 将 p 的根节点 指向 q 的根节点
@Override
public void union(int p, int q) {
// 分别获得q, p 的根节点的索引
int qRootIndex = find(q);
int pRootIndex = find(p);
if(qRootIndex == pRootIndex) return;
//如果q 高, 让 p 指向 q
if(height[qRootIndex] > height[pRootIndex]) {
data[pRootIndex] = qRootIndex;
//如果 p 高,让 q 指向 p
}else if(height[qRootIndex] < height[pRootIndex]){
data[qRootIndex] = pRootIndex;
}else{
data[qRootIndex] = pRootIndex;
height[pRootIndex] ++;
}
}
}
(5)基于路径压缩的优化
如果并查集中的树的高度越低,那么寻找根节点的速度就越快,效率就越高。路径压缩是一种将已将形成的树动态的降低树的高度的过程。无论是并查集的合并还是查询都必须寻找当前节点对应集合的根节点,那么如何在寻找过程中降低树的高度呢?首先看一下实现代码:
// 寻找 q 对应的根节点 对应的位置
@Override
public int find(int q) {
/*
让所有节点都直接指向根节点,通过递归实现
if(data[q] != q){
data[q] = find(data[q]);
}
*/
while(data[q] != q) {
data[q] = data[data[q]]; //************
q = data[q];
}
return q;
}
以上代码的data[q] = data[data[q]]
是重点,其动态压缩的过程如下: