10.并查集

一. 什么是并查集

应用

1.网络中节点间的连接状态
        - 网络是个抽象概念: 用户之间形成的网络

2.数学中集合的实现

 

对于一组数据, 主要支持两个动作

  • 并:union(p,q)
  • 查:isConnected(p,q)

 

接口

UF.java


public interface UF {

    int getSize();
    boolean isConnected(int p, int q);
    void unionElements(int p, int q);
}


二. 实现思路:Quick Find

并查集的基本数据表示

集合id1:    0 1 2 3 4
集合id2:    5 6 7 8 9

Quick Find 是查找 数据对应的 集合id号

 

简单地用数组实现并查集
UnionFind1.java

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 的集合编号
    private int find(int p){

        if(p < 0 && p >= id.length){
            throw new IllegalArgumentException("p is out of bound");
        }

        return id[p];
    }

    // 查看元素p和q是否为同一集合
    @Override
    public boolean isConnected(int p, int q){
        return find(p) == find(q);
    }

    //合并元素p和元素q所属的集合
    @Override
    public void unionElements(int p, int q){
        int pID = find(p);
        int qID = find(q);

        if(pID == qID){
            return;
        }

        for(int i =0; i < id.length; i++){
            if(id[i] == pID){
                id[i] = qID;
            }
        }
    }
}


三. 实现思路:Quick Union

过程理解

如下图:

节点7 和 节点3做union:
    节点7的根节点指向节点3的根节点

初始情况下, 每个节点都是一个树. 假设一开始有10个节点, 要实现union(4,3)。则如下图

 

在 union(9,8) union(3,8) union(2,1) union(6,5) union(5,0) union(7,2) union(6,2) 之后, 得到如下结果

 

代码实现

基于上面的逻辑以及 由孩子指向父亲的特点编写代码
UnionFind2.java

public class UnionFind2 implements UF {
    private int[] parent;

    public UnionFind2(int size) {
        parent = new int[size];

        for(int i = 0; i < size; i++){
            parent[i] = i;   //每个元素都是一棵树,parent是它自己     每棵树的根结点相当于之前的 集合id号
        }
    }

    @Override
    public int getSize(){
        return parent.length;
    }

    // 查找过程, 查找元素p所对应的集合编号
    // O(h)复杂度, h为树的高度
    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为树的高度
    @Override
    public boolean isConnected(int p, int q){
        return find(p) == find(q);
    }

    // 合并元素p和q所属的集合
    // O(h)的时间复杂度
    @Override
    public void unionElements(int p, int q){
        int pRoot = find(p);
        int qRoot = find(q);

        if(pRoot == qRoot){
            return;
        }

        parent[pRoot] = qRoot;
    }
}


四. 基于size优化

UnionFind1 与 UnionFind2 速度对比

Main.java

import java.util.Random;

public class Main {

    //执行m次 合并  和 查询操作
    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) {
        // write your code here
        int size = 100000;
        int m = 10000;

        UnionFind1 uf1 = new UnionFind1(size);

        System.out.println("Unifind1: " + testUF(uf1, m) + " s");


        UnionFind2 uf2 = new UnionFind2(size);
        System.out.println("Unifind2: " + testUF(uf2, m) + " s");


    }
}

结果(size=100000, m=10000)

Unifind1: 0.315987283 s
Unifind2: 0.014769037 s

将size不变, m=100000, 结果 :

Unifind1: 4.980790593 s
Unifind2: 10.85276505 s

Unionfind2 变慢了, 原因

1. Unionfind2 的find函数,在查询过程中会访问碎片化的地址空间。
    而UnionFind2的find函数 是基于数组的,java语言有优化
    
2. UnionFind2 的isConnected 复杂度 高于UnionFind1的isConnected, m的增大会使更多的元素组织在一个集合中。深度h会很高

 

基于size优化

原理: 
在Union(4,3)时,本来时4->3, 但如果 如果4的树深度大于3的树深度, 改为3->4 

 

UnionFind3.java(UnionFind2的基础上 加入了sz 并优化了unionElements)

public class UnionFind3 implements UF {
    private int[] parent;
    private int[] sz;  //sz[i]表示以i为根的集合元素个数

    public UnionFind3(int size) {
        parent = new int[size];
        sz = new int[size];

        for(int i = 0; i < size; i++){
            parent[i] = i;
            sz[i] = 1;
        }
    }

    @Override
    public int getSize(){
        return parent.length;
    }

    // 查找过程, 查找元素p所对应的集合编号
    // O(h)复杂度, h为树的高度
    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为树的高度
    @Override
    public boolean isConnected(int p, int q){
        return find(p) == find(q);
    }

    // 合并元素p和q所属的集合
    // O(h)的时间复杂度
    @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];
        }

    }
}


五. 基于rank的优化

原理

如下图所示情况:

 

如果让8->7, 会使合并后的树的深度增加。更合理的情况是7->8。 这样就是基于rank的优化

代码实现

在UnionFind3的基础上, sz改为rank 并修改unionElements函数
UnionFind4.java

public class UnionFind4 implements UF {
    private int[] parent;
    private int[] rank;  //rank[i]表示以i为根的树 的 层数(深度)

    public UnionFind4(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;
    }

    // 查找过程, 查找元素p所对应的集合编号
    // O(h)复杂度, h为树的高度
    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为树的高度
    @Override
    public boolean isConnected(int p, int q){
        return find(p) == find(q);
    }

    // 合并元素p和q所属的集合
    // O(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[pRoot] < rank[qRoot]){
            parent[qRoot] = pRoot;
        }
        else{
            parent[qRoot] = pRoot;
            rank[pRoot] += 1;
        }

    }
}

在Main.java中测试,得到结果如下

Unionfind1: 5.040893966 s
Unionfind2: 10.61027382 s
Unionfind3: 0.015299379 s
Unionfind4: 0.012385086 s

六.基于路径压缩的优化

优化原理

如下图中的三种路径其实是等价的, 我们可以根据图中思路来压缩深度h 

 

我们将压缩路径的代码放在find函数中

在寻找根节点的过程中, 执行:
    parent[p] = parent[parent[p]]
即在向上寻找时 本来指向父节点的 改为指向爷爷节点

代码实现

UnionFind5.java 基于UnionFind4 只要在find中增加一行代码

 ...
    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;
    }
...

Main中进行速度测试

Main.java

...

    public static void main(String[] args) {
        // write your code here
        int size = 10000000;
        int m = 10000000;

//        UnionFind1 uf1 = new UnionFind1(size);
//
//        System.out.println("Unionfind1: " + testUF(uf1, m) + " s");
//
//
//        UnionFind2 uf2 = new UnionFind2(size);
//        System.out.println("Unionfind2: " + testUF(uf2, m) + " s");

        UnionFind3 uf3 = new UnionFind3(size);
        System.out.println("Unionfind3: " + testUF(uf3, m) + " s");

        UnionFind4 uf4 = new UnionFind4(size);
        System.out.println("Unionfind4: " + testUF(uf4, m) + " s");

        UnionFind5 uf5 = new UnionFind5(size);
        System.out.println("Unionfind5: " + testUF(uf5, m) + " s");
    }
}

结果

Unionfind3: 5.252558689 s
Unionfind4: 5.062379705 s
Unionfind5: 3.984109604 s

最优的压缩

如果我们想让每棵树都路径压缩到 深度只有2, 我们需要使用递归

UnionFind6基于UnionFind5, 只修改find函数

...

    private int find(int p){
        if(p < 0 && p >= parent.length){
            throw new IllegalArgumentException("p is out of bound.");
        }

        if(p != parent[p]){
            parent[p] = find(parent[p]);   //使用递归, 直接取到根节点
        }
        return parent[p];
    }
...

速度测试结果:

Unionfind3: 5.240926873 s
Unionfind4: 4.854475902 s
Unionfind5: 3.90928111 s
Unionfind5: 4.298501956 s     因为递归的消耗,UnionFind5优于UnionFind6

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/81149196
10.