算法-并查集
标签:算法

并查集(Union Find)

并查集可以很高效的解决连接问题 (Connectivity Problem)

网络中节点间的连接状态

  • 网络是个抽象的概念: 用户之间形成的网络

数学中的集合

1. 实现方式一

首先设置每个元素在id数组中的值就是自己,find操作就是直接返回当前元素所对应的id数组中的值,如果在同一个集合的话,那么他们的值应该相同的。合并操作就是首先找到两个元素的id值,如果相同则表明他们属于同一个集合,直接返回,否者遍历所有的元素,将值等于p的id的元素全部改为q的id即可。上面这种操作实现简单,但效率较低。

package com.liuyao.union;

/**
 * Created By liuyao on 2018/4/29 17:08.
 */
public class UnionFind1 {
    private int[] id; // 我们的第一版Union-Find本质就是一个数组
    private int count; // 数据个数

    public UnionFind1(int n) {
        count = n;
        id = new int[n];
        // 初始化, 每一个id[i]指向自己, 没有合并的元素
        for (int i = 0; i < n; i++) {
            id[i] = i;
        }
    }

    private int find(int p) {
        assert p >= 0 && p < count;
        return id[p];
    }

    // 查看元素p和元素q是否所属一个集合
    // O(1)复杂度
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
    // O(n) 复杂度
    public void unionElements(int p, int q) {
        int pID = find(p);
        int qID = find(q);

        if (pID == qID) {
            return;
        }

        // 合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并
        for (int i = 0; i < count; i++) {
            if (id[i] == pID) {
                id[i] = qID;
            }
        }
    }

    public static void main(String[] args) {
        int n = 100000;
        UnionFindTestHelper.testUF1(n);
    }
}

2. 实现方式二

最开始初始化的时候,每个元素都将父节点指向自己,两个元素union时,就将这个元素的parent指向一个元素的parent即可。unionElements时先判断两个节点的最前面的parent是否相等,相等则返回,不等则将前面的这个元素的最前面的parent指向后面的这个元素的最前面的parent。

package com.liuyao.union;

/**
 * Created By liuyao on 2018/4/29 17:43.
 */
public class UnionFind2 {
    private int[] parent; //parentb[i]表示第一个元素所指向的父节点
    private int count; //数据个数

    public UnionFind2(int count) {
        parent = new int[count];
        // 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
        for (int i = 0; i < count; i++) {
            parent[i] = i;
        }
    }

    // 查找过程, 查找元素p所对应的集合编号
    // O(h)复杂度, h为树的高度
    private int find(int p) {
        assert (p >= 0 && p < count);
        // 不断去查询自己的父亲节点, 直到到达根节点
        // 根节点的特点: parent[p] == p
        while (p != parent[p]) {
            p = parent[p];
        }
        return p;
    }

    // 查看元素p和元素q是否所属一个集合
    // O(h)复杂度, h为树的高度
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
    // O(h)复杂度, h为树的高度
    public void unionElements(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot == qRoot) {
            return;
        }
        parent[pRoot] = qRoot;
    }

    public static void main(String[] args) {
        int n = 100000;

        UnionFindTestHelper.testUF2(n);
    }
}

3. 实现方式三——size优化

基于size的优化,每次unionElements的时候先判断两个元素的size的大小,每次将小的个数的元素链接到大的,这样可以避免每次都是左边链接到右边而形成了链表的情况发生。

package com.liuyao.union;

/**
 * Created By liuyao on 2018/4/29 17:43.
 */
public class UnionFind3 {
    private int[] parent; //parentb[i]表示第一个元素所指向的父节点
    private int count; //数据个数
    private int[] sz; //当前根下的元素个数

    public UnionFind3(int count) {
        parent = new int[count];
        sz = new int[count];
        // 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
        for (int i = 0; i < count; i++) {
            parent[i] = i;
            sz[i] = 1; //初始的时候每个集合里面只有自己一个元素
        }
    }

    //其他操作类似

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

4. 实现方式四——rank优化

此时根据每个集合对应高度来比较,然后选择高度小的做为根,不更新rank值,如果两个高度相同,则任意选择,但此时两个parent相连,高度将会加一。

package com.liuyao.union;

/**
 * Created By liuyao on 2018/4/29 17:43.
 */
public class UnionFind4 {
    private int[] parent; //parentb[i]表示第一个元素所指向的父节点
    private int count; //数据个数
    private int[] rank; //当前根下的元素个数

    public UnionFind4(int count) {
        parent = new int[count];
        rank = new int[count];
        // 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
        for (int i = 0; i < count; i++) {
            parent[i] = i;
            rank[i] = 1;
        }
    }

	//其他操作类似

    // 合并元素p和元素q所属的集合
    // O(h)复杂度, h为树的高度
    public void unionElements(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot == qRoot) {
            return;
        }
        // 根据两个元素所在树的元素个数不同判断合并方向
        // 将元素个数少的集合合并到元素个数多的集合上
        if (rank[pRoot] < rank[qRoot]) {
            parent[pRoot] = qRoot;
        } else if (rank[pRoot] > rank[qRoot]) {
            parent[qRoot] = pRoot;
        } else {
            parent[pRoot] = qRoot;
            rank[qRoot] += 1; //qRoot作为根,高度加一
        }
    }

}

5. 实现方式五——路径压缩

    // 查找过程, 查找元素p所对应的集合编号
    // O(h)复杂度, h为树的高度
    private int find(int p) {
        assert (p >= 0 && p < count);
        // 不断去查询自己的父亲节点, 直到到达根节点
        // 根节点的特点: parent[p] == p
        while (p != parent[p]) {
            parent[p] = parent[parent[p]];
            p = parent[p];
        }
        return p;
    }

通过递归的方式找到最后的根节点并返回。再将其他元素的parent设置为它。

    // 查找过程, 查找元素p所对应的集合编号
    // O(h)复杂度, h为树的高度
    private int find(int p) {
        assert (p >= 0 && p < count);
        // 不断去查询自己的父亲节点, 直到到达根节点
        // 根节点的特点: parent[p] == p
        
        //递归算法
        if (p != parent[p]) {
            parent[p] = find(parent[p]);  //递归查找并做该结合中的元素都的根节点都变成第一个元素
        }
        return parent[p];
    }
  • 7 min read

CONTRIBUTORS


  • 7 min read