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

789 阅读5分钟

quick-union样本分析

在上一次的测试验证中,可以得出quick-union相对于quick-find算法有些优势,但是在连通数据集增大的情况下,quick-union的表现不是很好,仍需要分析下在不同的数据集中,quick-union算法的表现。

最优情况

最优的情况,就是规避掉了quick-union的劣势,寻找分量标识的时候也能通过O(1)时间就找到,举个例子,如果连通数据集如下:

序号pq
001
002
003
004
005
006
007
008
009

合并后结果:

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

化成图形

image.png

无疑,可以直观的感受到,在每次合并操作的时候,都是将新分量直接合并到分量0的标识下,分量1到9是平级机构,每个分量都可以经过一次查找找到自己对应的分量标识。这也就是在特殊的数据情况下,quick-union可以达到quick-find的效果,而且不用遍历!

最差情况

最差情况下,有多少个分量就有多少的深度,那么每次find操作,时间复杂度就是O(n),还是举个例子还说明一下。(为了便于说明问题,quick-union的代码有一处改动)

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

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

        // 如果不相等,直接让分量q的标识指向p
        id[pId] = qId;
        // 每次合并操作,会降低一个不同分量组
        count--;
    }
序号pq
001
012
023
034
045
056
067
078
089

合并后结果:

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

化成图形 image.png

也可以直观的看到,此时并查集严重不平衡,对算法的执行效率也会大打折扣。

针对这种情况,优化的目的也变得很清晰,我们要尽量维护这课树的平衡,尽量避免这种不平衡的局面出现,自然就可以提升算法的性能。

加权quick-union

从最差的样本中我们可以发现,之所以形成了这种极端不平衡的链表样式的树,是因为每次操作都是把大树合并到小树中,比如在执行最后一次p=8,q=9的连通合并操作时,我们的操作如下

image.png

我们在为加权的情况下,合并之后形成了 image.png

然而我们希望的结果应该是 image.png

所以,我们假设有一个变量来比较待合并的两个分量权重的大小,我们始终把权重小的分量合并到权重大的分量下,那么这种极端的倾斜树,应该可以得到好转。

经过上面的验证和分析,我们来分析加权quick-union的处理步骤

  • 在quick的基础上,添加一个成员属性,标识分量的权重
  • 在合并操作上,不再是简单的将一个分量归属到另一个分量下,而是进行一次权重的比较,将权重小的分量,合并到权重大的分量下。

直接看代码

package com.zt;

public class WeightedQuickUnionUnionFind extends UnionFind {

    // 分量权重数组,数组里的值代表当前分量下的分量数量
    private int[] weightArr ;

    public WeightedQuickUnionUnionFind(int n) {

        super(n);
        weightArr = new int[n];
        // 初始化权重数组,初始化是每个分量下的权重都为1
        for (int i = 0; i < weightArr.length; i++) {
            weightArr[i] = 1;
        }
    }

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

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

        // 将权重小的树,合并到权重大的树下
        if (weightArr[p] < weightArr[q]) {
            id[p] = q;
            weightArr[q] += weightArr[p];
        } else {
            id[q] = p;
            weightArr[p] += weightArr[q];
        }

        // 每次合并操作,会降低一个不同分量组
        count--;
    }

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

验证效果

再次验证合并后的最差数据集结果

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

可以看到经过加权操作后,quick-union变成了一颗扁平的树,打到了最优化的结果。

随机数据集

因为数据比较特殊,我们随机生成一组样本数据再次进行一下比较。

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

未加权结果

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

image.png

加权结果

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

image.png

总结

经过了加权操作的quick-union可以明显的优化quick-union的劣势,可以将树的深度控制在log(n)级别,甚至更优,经过加权后的并查集又经历了更进一步的演化。

展望

加权的quick-union其实还没有达到最优化的处理,有没有什么办法可以让quick-union只有一层呢?从O(log(n)) 的时间复杂度升级为O(1)? 下一次让我们继续探索一下。