一、引言
并查集是一种比较独特的数据结构,它是由孩子节点指向父亲节点的树形结构。并查集能高效地解决连接问题(Connectivity Problem)
- 连接问题
说明:上图就是一个连接问题,左上角两个点直接是否被连接,肉眼观察就能给出肯定的答案,如果问左上角的一点与右下角的一点是否是连接的,用肉眼就很难观察了。并查集正是高效解决这一问题的数据结构。
二、实现
- 实现方式1(Quick Find)
/**
* 基于数组模拟并查集第一版(Quick Find)
* 查询操作的时间复杂度是:O(1)
* 合并操作的时间复杂度是:O(n)
*/
public class UnionFind1 implements UF {
private int[] id;
public UnionFind1(int size) {
id = new int[size];
for (int i = 0; i < id.length; i++) {
id[i] = i;
}
}
@Override
public int getSize() {
return id.length;
}
/**
* 查找元素p所对应的集合编号
* @param p 元素ID
* @return
*/
private int find(int p) {
if (p < 0 && p >= id.length) {
throw new IllegalArgumentException("p is out of bound");
}
return id[p];
}
/**
* 查看元素p和元素q是否所属同一个集合
* @param p 元素ID
* @param q 元素ID
* @return
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
/**
* 合并元素p和元素q所属的集合
* @param p 元素ID
* @param q 元素ID
*/
@Override
public void unionElements(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID) {
return;
}
//将所有属于p元素所在的集合编号覆盖为q元素所属的集合编号
for (int i = 0; i < id.length; i++) {
if (id[i] == pID) {
id[i] = qID;
}
}
}
}
说明:以上的实现方式基于数组实现,数组中值不同则表示元素属于不同的集合;查询操作的时间复杂度是O(1),union操作的时间复杂度是O(n),因为需要遍历数组中各个元素,判断是否需要修改集合序号。
- 实现方式2(Quick Union)
/**
* 基于数组模拟并查集第二版
*/
public class UnionFind2 implements UF {
private int[] parent;
public UnionFind2(int size) {
parent = new int[size];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
}
}
@Override
public int getSize() {
return parent.length;
}
/**
* 查找元素p所对应的根节点集合编号
* 时间复杂度为O(h),h为树的高度
* @param p
* @return
*/
private int find(int p) {
if (p < 0 && p >= parent.length) {
throw new IllegalArgumentException("p is out of bound");
}
while (p != parent[p]) {
p = parent[p];
}
return p;
}
/**
* 查看元素p和元素q是否所属同一个集合
* 时间复杂度为O(h),h为树的高度
* @param p 元素ID
* @param q 元素ID
* @return
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
parent[pRoot] = qRoot;
}
}
说明:Quick Union是基于数组的树结构,查询和合并操作的时间复杂度是O(h),跟树的深度相关。相比Quick Find方式牺牲了点查询性能,但是合并性能得到了提升。
- 基于集合树的size优化
/**
* 基于数组模拟并查集第三版
* 基于size元素个数对并查集合并操作进行优化
*/
public class UnionFind3 implements UF {
private int[] parent;
/**
* sz[i]表示以i为根的集合中元素个数
*/
private int[] sz;
public UnionFind3(int size) {
parent = new int[size];
sz = new int[size];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
sz[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
/**
* 查找元素p所对应的根节点集合编号
* 时间复杂度为O(h),h为树的高度
* @param p
* @return
*/
private int find(int p) {
if (p < 0 && p >= parent.length) {
throw new IllegalArgumentException("p is out of bound");
}
while (p != parent[p]) {
p = parent[p];
}
return p;
}
/**
* 查看元素p和元素q是否所属同一个集合
* 时间复杂度为O(h),h为树的高度
* @param p 元素ID
* @param q 元素ID
* @return
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
//根据两个元素所在书的元素个数不同判断合并方向
//将元素个数少的集合合并到元素个数多的集合上
if (sz[pRoot] < sz[qRoot]) {
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
} else {
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
}
说明:在Quick Union的基础上,为了避免树的深度太大,极端情况下树会退化成链表形式,所以考虑了树的size,小集合树指向大集合树,而避免了大集合树指向小集合树,有效避免了集合树深度过大问题。
- 集合树的rank优化
/**
* 基于数组模拟并查集第四版
* 基于rank深度对并查集合并操作进行优化
*/
public class UnionFind4 implements UF {
private int[] parent;
/**
* rank[i]表示以i为根的集合所表示的树的层数
*/
private int[] rank;
public UnionFind4(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
rank[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
/**
* 查找元素p所对应的根节点集合编号
* 时间复杂度为O(h),h为树的高度
* @param p
* @return
*/
private int find(int p) {
if (p < 0 && p >= parent.length) {
throw new IllegalArgumentException("p is out of bound");
}
while (p != parent[p]) {
p = parent[p];
}
return p;
}
/**
* 查看元素p和元素q是否所属同一个集合
* 时间复杂度为O(h),h为树的高度
* @param p 元素ID
* @param q 元素ID
* @return
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
//根据两个元素所在树的rank不同判断合并方向
//将rank低的集合合并到rank高的集合上
if (rank[pRoot] < rank[qRoot]) {
parent[pRoot] = qRoot;
} else if (rank[pRoot] > rank[qRoot]) {
parent[qRoot] = pRoot;
} else {
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}
}
}
说明:在基于树size的优化基础上,进一步优化,树的size大,但是树的深度不一定就大,比较合理的优化方式是基于树的高度优化,高度小的树指向高度大的树。这样合并操作以后尽量不会增加树的高度。
- 并查集的路径压缩优化(Path Compression)
/**
* 基于数组模拟并查集第四版
* 基于路径压缩对并查集合并操作进行优化
*/
public class UnionFind5 implements UF {
private int[] parent;
/**
* rank[i]表示以i为根的集合所表示的树的rank
*/
private int[] rank;
public UnionFind5(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
rank[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
/**
* 查找元素p所对应的根节点集合编号
* 时间复杂度为O(h),h为树的高度
* @param p
* @return
*/
private int find(int p) {
if (p < 0 && p >= parent.length) {
throw new IllegalArgumentException("p is out of bound");
}
while (p != parent[p]) {
//路径压缩,降低树的深度
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
/**
* 查看元素p和元素q是否所属同一个集合
* 时间复杂度为O(h),h为树的高度
* @param p 元素ID
* @param q 元素ID
* @return
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
//根据两个元素所在树的rank不同判断合并方向
//将rank低的集合合并到rank高的集合上
if (rank[pRoot] < rank[qRoot]) {
parent[pRoot] = qRoot;
} else if (rank[pRoot] > rank[qRoot]) {
parent[qRoot] = pRoot;
} else {
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}
}
}
说明:在查询操作时,会对树路径进行压缩,将高度比较高的树压缩成高度较低的树。提升查询和合并的效率。
- 并查集的路径压缩2
/**
* 基于数组模拟并查集第四版
* 基于路径压缩对并查集合并操作进行优化
*/
public class UnionFind6 implements UF {
private int[] parent;
/**
* rank[i]表示以i为根的集合所表示的树的rank
*/
private int[] rank;
public UnionFind6(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
rank[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
/**
* 查找元素p所对应的根节点集合编号
* 时间复杂度为O(h),h为树的高度
* @param p
* @return
*/
private int find(int p) {
if (p < 0 && p >= parent.length) {
throw new IllegalArgumentException("p is out of bound");
}
if (p != parent[p]) {
//路径压缩,递归找到p元素的根,然后直接将p元素挂到根上,最大限度的压缩
parent[p] = find(parent[p]);
}
return parent[p];
}
/**
* 查看元素p和元素q是否所属同一个集合
* 时间复杂度为O(h),h为树的高度
* @param p 元素ID
* @param q 元素ID
* @return
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
//根据两个元素所在树的rank不同判断合并方向
//将rank低的集合合并到rank高的集合上
if (rank[pRoot] < rank[qRoot]) {
parent[pRoot] = qRoot;
} else if (rank[pRoot] > rank[qRoot]) {
parent[qRoot] = pRoot;
} else {
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}
}
}
说明:路径压缩的另一种方式,利用递归将树压缩到最理想的两层。最大程度的提升路径查询和元素合并操作的性能。
三、简单性能测试
- 针对以上各种方式的并查集实现,简单本地测试如下
import java.util.Random;
public class Main {
public static void main(String[] args) {
//UnionFind1(size=100000,m=10000) : 0.2367079 s
testUnionFind1(100000, 10000);
//UnionFind1(size=100000,m=100000) : 4.482173601 s
testUnionFind1(100000, 100000);
//UnionFind2(size=100000,m=10000) : 0.0016591 s
testUnionFind2(100000, 10000);
//UnionFind2(size=100000,m=100000) : 9.875075801 s
testUnionFind2(100000, 100000);
//UnionFind3(size=20000000,m=20000000) : 9.055730401 s
testUnionFind3(20000000, 20000000);
//UnionFind4(size=20000000,m=20000000) : 9.4252049 s
testUnionFind4(20000000, 20000000);
//UnionFind5(size=20000000,m=20000000) : 8.417531 s
testUnionFind5(20000000, 20000000);
//UnionFind6(size=20000000,m=20000000) : 7.6787675 s
testUnionFind6(20000000, 20000000);
}
private static void testUnionFind1(int size, int m) {
UnionFind1 uf1 = new UnionFind1(size);
System.out.println("UnionFind1(size=" + size + ",m=" + m + ") : " + testUF(uf1, m) + " s");
}
private static void testUnionFind2(int size, int m) {
UnionFind2 uf2 = new UnionFind2(size);
System.out.println("UnionFind2(size=" + size + ",m=" + m + ") : " + testUF(uf2, m) + " s");
}
private static void testUnionFind3(int size, int m) {
UnionFind3 uf3 = new UnionFind3(size);
System.out.println("UnionFind3(size=" + size + ",m=" + m + ") : " + testUF(uf3, m) + " s");
}
private static void testUnionFind4(int size, int m) {
UnionFind4 uf4 = new UnionFind4(size);
System.out.println("UnionFind4(size=" + size + ",m=" + m + ") : " + testUF(uf4, m) + " s");
}
private static void testUnionFind5(int size, int m) {
UnionFind5 uf5 = new UnionFind5(size);
System.out.println("UnionFind5(size=" + size + ",m=" + m + ") : " + testUF(uf5, m) + " s");
}
private static void testUnionFind6(int size, int m) {
UnionFind6 uf6 = new UnionFind6(size);
System.out.println("UnionFind6(size=" + size + ",m=" + m + ") : " + testUF(uf6, m) + " s");
}
private static double testUF(UF uf, int m) {
int size = uf.getSize();
Random random = new Random();
long startTime = System.nanoTime();
//m次合并操作
for (int i = 0; i < m; i++) {
int a = random.nextInt(size);
int b = random.nextInt(size);
uf.unionElements(a, b);
}
for (int i = 0; i < m; i++) {
int a = random.nextInt(size);
int b = random.nextInt(size);
uf.isConnected(a, b);
}
long endTime = System.nanoTime();
return (endTime-startTime) / 1000000000.0;
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。