接着上篇介绍并查集的优化方法
3.路径压缩
上一篇介绍了基于rank的优化,但是依然有一定的问题需要解决,因为当我们的数据量越来越大时,树的高度可能也会随之变大,数据量小的时候影响不大,但当数据量很大时,树的高度比较高,查询数据就会变慢了,所以我们有必要通过减小树的高度而不破坏原先的连接关系的方法来解决以上问题。我们可以在每次查询时将树的高度缩减,这就是路径压缩。
如上图,三个图表示的结果是相同的,每一棵树的任意两个节点都是连接的,但是树的高度却不同,2图比较理想的压缩结果,所有的节点都直接连在一个根节点上,树的高度始终为2,但是比较难以实现。所以退而求其次,将1图的树压缩成3图的树,高度以减小了。
压缩过程:每次执行查询方法时,都执行以下parent[i] = parent[parent[i]],也就是每次查询时先判断要查询的节点的父节点是否是根节点,不是的话让当前节点指向当前节点的父节点的父节点,完成一次压缩,如果要查询的节点的父节点的父节点还不是根节点,就继续向上遍历,直到要查询的节点的父节点的父节点是根节点。
代码实现:
public class UnionFind5 implements UF {
int[] parent;
int[] rank; //rank[i] 表示以i为根节点的集合所表示的树的层数
public UnionFind5(int size){
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
//查找i对应的集合编号
//时间复杂度为O(h),h为树的高度
private int find(int i){
if(i < 0 || i>= parent.length)
throw new IllegalArgumentException("非法索引");
while (i != parent[i]) {
parent[i] = parent[parent[i]]; //每次i循环向上寻找根节点的过程中,都将i重新指向它的根节点的根节点,从而使树的深度变浅
i = parent[i];
}
return i;
}
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
//合并操作
//时间复杂度为O(h),h为树的高度
@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[qRoot] < rank[pRoot]){
parent[qRoot] =pRoot;
}
else{ //rank[pRoot] == rank[qRoot] 以pRoot和qRoot为根节点的两棵树层数一样时,合并时可以将任意一个合并到另一个即可
parent[pRoot] = qRoot;
rank[qRoot] += 1; //合并完成后层数加1
}
}
}
性能测试:
测试代码
public class Test {
private static double testUF(UF uf,int m){
int size = uf.getSize();
Random random = new Random();
long startTime = System.nanoTime();
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;
}
public static void main(String[] args) {
int size = 10000000;
int m = 10000000;
UnionFind1 unionFind1 = new UnionFind1(size);
/* System.out.println("UnionFind1: " + testUF(unionFind1,m) + " s");
UnionFind2 unionFind2 = new UnionFind2(size);
System.out.println("UnionFind2: " + testUF(unionFind2,m) + " s");*/
UnionFind3 unionFind3 = new UnionFind3(size);
System.out.println("UnionFind3: " + testUF(unionFind3,m) + " s");
UnionFind4 unionFind4 = new UnionFind4(size);
System.out.println("UnionFind4: " + testUF(unionFind4,m) + " s");
UnionFind5 unionFind5 = new UnionFind5(size);
System.out.println("UnionFind5: " + testUF(unionFind5,m) + " s");
/* UnionFind6 unionFind6 = new UnionFind6(size);
System.out.println("UnionFind6: " + testUF(unionFind6,m) + " s");*/
}
}
执行结果
路径压缩后的性能有了显著的提升。
上一篇中维护了rank来做合并时的比较,上篇当时说的是在没有介绍路径压缩之前可以把rank理解为树的高度或深度,这篇介绍了路径压缩后这个概念就变了,因为我们在压缩树的过程中,树的高度或深度变小了,而我们这里并没有修改rank,所以rank就不在代码树的高度或深度,而是作为一个排序,在合并时依然可以比较rank大小进行合并,将rank小的树指向rank大的树,rank的功能没有变,依然可以胜任这个合并时的比较工作。
4.理想化的路径压缩
在上面介绍了理想化的路径压缩比价难实现,但是并不是不可以实现,我们完全可以借助递归方法把图一中的第一棵树压缩成图一中的第二棵树的形状,只是这种理想化的压缩对性能提升不了多小,甚至可能使性能稍微变差,因为递归调用是要耗费性能的。
代码实现:
public class UnionFind6 implements UF {
int[] parent;
int[] rank; //rank[i] 表示以i为根节点的集合所表示的树的层数
public UnionFind6(int size){
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
//查找i对应的集合编号
//时间复杂度为O(h),h为树的高度
private int find(int i){
if(i < 0 || i>= parent.length)
throw new IllegalArgumentException("非法索引");
if (i != parent[i]) {
parent[i] = find(parent[i]);
}
return parent[i];
}
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
//合并操作
//时间复杂度为O(h),h为树的高度
@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[qRoot] < rank[pRoot]){
parent[qRoot] =pRoot;
}
else{ //rank[pRoot] == rank[qRoot] 以pRoot和qRoot为根节点的两棵树层数一样时,合并时可以将任意一个合并到另一个即可
parent[pRoot] = qRoot;
rank[qRoot] += 1; //合并完成后层数加1
}
}
}
性能测试:
代码同上,只加两句
UnionFind6 unionFind6 = new UnionFind6(size);
System.out.println("UnionFind6: " + testUF(unionFind6,m) + " s");
执行结果