[并查集]并查集的升级路线(二)

332 阅读5分钟

quick-union

quick-find是为了快速找到树的标识,quick-union顾名思义就是为了快速合并。

quick-find算法特点

  • find时间复杂度O(1)
  • union时间复杂度是O(n)

quick-union算法特点

  • find最差情况下时间复杂度O(n),平均O(log(n))
  • union时间复杂度是O(1)

两者算法时间复杂度比较

quick-findquick-union
findO(1)最差情况O(n),平均情况O(log(n))
unionO(n)O(1)

之所以quick-union可已实现O(1)时间复杂度的合并操作,用两个分量a,b来解释,那是因为在连通过程中直接将b分量的标识指向了a,那么所有标识为b的分量都会随着b转移到a分量所在的树中。

具体实现步骤

  • 定义QuickUnionUnionFind类继承UnionFind
  • 实现find方法,找到数组下标与值相同的元素即为该树的标识元素
  • 实现union方法,合并p,q时,将q的标识指向p的下标。
package com.zt;

public class QuickUnionUnionFind extends UnionFind {
    public QuickUnionUnionFind(int n) {
        super(n);
    }

    @Override
    void union(int p, int q) {
        // 找到p的标识
        int pId = find(p);
        // 找到q的标识
        int qId = find(q);

        // 如果两个标识相等,代表已经合并
        if (pId == qId) return;

        // 如果不相等,直接让分量q的标识指向p
        id[qId] = pId;
        // 每次合并操作,会降低一个不同分量组
        count--;
    }

    @Override
    int find(int p) {
        // 沿着标识路径一路寻找,直到找到树的标识
        while (p != id[p]) p = id[p];
        return p;
    }
}

quick-find与quick-union的比较

为了说明算法的优劣,需要用实际的数据来进行比较说明,首先准备了一个构造数据的方法,该方法可以为我们动态生成一个连通集。用二维数组来表示,每一行有两个分量,代表两个分量存在连通关系,不同行代表不同的连通关系。

public static int[][] setUpTestData10(int n, int digit) {
        int[][] res = new int[n][2];

        for (int i = 0; i < res.length; i++) {
            Random random = new Random();
            res[i] = new int[]{random.nextInt(digit), random.nextInt(digit)};
        }

        return res;

    }

小数据量测试

首先准备测试用代码如下。

package com.zt;

import org.omg.CORBA.OBJ_ADAPTER;

import java.util.Random;

public class UnionFindHelper {

    public static void main(String[] args) {

        int num = 10;
        int[][] res = setUpTestData(num, num);
        QuickFindUnionFind qfuf = new QuickFindUnionFind(num);
        QuickUnionUnionFind quuf = new QuickUnionUnionFind(num);
        execute(res,qfuf);

        System.out.println("****************************");
        System.out.println("****************************");
        System.out.println("****************************");

        execute(res,quuf);
    }

    public static void execute(int[][] res,  UnionFind uf) {

        soutuf(uf);
        soutLine("行数", "p", "q", "耗时");
        long timeStart = System.currentTimeMillis();
        for (int i = 0; i < res.length; i++) {
            int p = res[i][0];
            int q = res[i][1];
            long timeStartOne = System.currentTimeMillis();
            uf.union(p, q);
            long timeEndOne = System.currentTimeMillis();
            soutLine(i, p, q, timeEndOne - timeStartOne);
        }
        long timeEnd = System.currentTimeMillis();
        System.out.println();
        soutuf(uf);

        System.out.println("总耗时时间" + (timeEnd - timeStart));
    }

    public static void soutuf(UnionFind uf) {
        System.out.println("-------------------------------");
        for (int i = 0; i < uf.id.length; i++) {
            System.out.printf("%5d", i);
        }

        System.out.println();
        System.out.println("-------------------------------");
        for (int i = 0; i < uf.id.length; i++) {
            System.out.printf("%5d", uf.id[i]);
        }
        System.out.println();
        System.out.println("-------------------------------");
    }

    public static void soutLine(Object... args) {

        String format = "";
        for (Object arg : args) {
            format = format + "%5s";
        }
        System.out.printf(format, args);
        System.out.println();
    }

    public static int[][] setUpTestData(int n, int digit) {
        int[][] res = new int[n][2];

        for (int i = 0; i < res.length; i++) {
            Random random = new Random();
            res[i] = new int[]{random.nextInt(digit), random.nextInt(digit)};
        }

        return res;

    }

}

构造一个十个元素的数据,初始时数组每个值都指向自己的下标

-------------------------------
    0    1    2    3    4    5    6    7    8    9
-------------------------------
    0    1    2    3    4    5    6    7    8    9

quick-find执行结果

-------------------------------
    0    1    2    3    4    5    6    7    8    9
-------------------------------
    4    4    4    3    4    3    6    4    4    4
-------------------------------
总耗时时间2

quick-union执行结果

-------------------------------
    0    1    2    3    4    5    6    7    8    9
-------------------------------
    4    4    7    3    4    3    6    0    1    0
-------------------------------
总耗时时间1

小数据量分析

从小数据量可以看出,quick-find的并查集构建结果,存在大量相同的标识,直观上可以通过不同的值就看出来并查集这个森林中有多少颗树,而在quick-union中,标识较为散乱,直观上看不出树的个数;全部数据处理的耗时上,两者只相差了1毫秒,看起来差别不大。接下来数据量进行扩大试验一次。

去掉日志打印代码后,数据统计如下

1010^210^310^410^510^6
quick-find025485379超时
quick-union00161531超时

数据增长方式是采用同步增长,指数增加一级,数据量增加一级,换个方式,在分量总数保持在10^4情况下,增加数据量,观测两种算法表现。

1倍2倍3倍10^410^510^6
quick-find486366677168
quick-union65990138172220

可以观察到,在分量总数不变的情况下,随着连通数据集的增多,quick-find保持在一个稳定的时间内,而quick-union会随之不断增长,内在的原因是什么呢?

答案在这里 image.png

因为每次union操作都会进行两次查找,在quick-find算法中,该时间复杂度是O(1),在quick-union算法中,该复杂度最差情况下会达到O(n)

##总结

经过数据的验证与测试,我们可以总结道一下两点

  • 随着分量总数增加,quick-union表现比quick-find要好,因为分量总量即id变多导致quick-find的union操作时间变高。
  • 随着连通数据总量增加,quick-find相较quick-union表现要好,是因为连通数据量变大,导致quick-union的find操作耗时增长。
  • 两者在处理10^6量级的数据时,都表现超时。

展望

总结过后,两种算法各有又是劣势,但是相对来说,在分量总数增多的情况下quick-union是线性增长,而quick-find是指数级增长,相比较而言,quick-union更升级了一点,那么有没有办法融合两种算法的优势呢?下一节继续探索并查集的升级路线。