一、引言

并查集是一种比较独特的数据结构,它是由孩子节点指向父亲节点的树形结构。并查集能高效地解决连接问题(Connectivity Problem)

  • 连接问题

连接问题.png

说明:上图就是一个连接问题,左上角两个点直接是否被连接,肉眼观察就能给出肯定的答案,如果问左上角的一点与右下角的一点是否是连接的,用肉眼就很难观察了。并查集正是高效解决这一问题的数据结构。

二、实现

  • 实现方式1(Quick Find)

Quick Find基本数据表示.png

/**
 * 基于数组模拟并查集第一版(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)

Quick Union基本数据表示.png

一系列合并操作后的数据表示.png

/**
 * 基于数组模拟并查集第二版
 */
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优化

大集合树指向小集合树.png

小集合树指向大集合树.png

/**
 * 基于数组模拟并查集第三版
 * 基于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优化

image.png

小集合树指向大集合树.png

基于树的rank深度优化.png

/**
 * 基于数组模拟并查集第四版
 * 基于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)

路径压缩.png

压缩前.png

压缩后.png

/**
 * 基于数组模拟并查集第四版
 * 基于路径压缩对并查集合并操作进行优化
 */
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

image.png

/**
 * 基于数组模拟并查集第四版
 * 基于路径压缩对并查集合并操作进行优化
 */
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;
    }
}

四、其它数据结构


neojayway
52 声望10 粉丝

学无止境,每天进步一点点