美文网首页
[并查集]并查集的升级路线(四)

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

作者: 铜炉 | 来源:发表于2021-02-14 13:46 被阅读0次

路径压缩并查集

并查集在经过了quick-find、quick-union、加权quick-union的演化之后,我们拥有了一个暂时看起来非常不错的算法,就是加权quick-union。经过分析和总结后,加权quick-union的操作可以将树的深度控制在log(n)的级别,相较于单纯的quick-union已然是一个不错的选择,但是进一步优化,可不可以将log(n)提升呢?甚至打到O(1)的时间复杂度?

要想在进一步优化,那么就需要保证并查集中每一颗树的深度都可以在常熟级别内,那么,需要做的事情,就是在树的构建过程中,不断把路径压缩,从而达到减少find操作的深度。

举个例子

image.png

图中所示,这是一颗不平衡的树,从根节点的4到叶子节点的0一共有5层,如果我们将0的父节点不再是指向1,而是指向2,那么树的深度也就可以由5层变为4层,完成了路径压缩。如下图

image.png

那么问题来了,应该在哪一个环节进行这样的改造呢?

还是从代码层面来分析一下

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[pId] < weightArr[qId]) {
            id[pId] = qId;
            weightArr[qId] += weightArr[pId];
        } else {
            id[qId] = pId;
            weightArr[pId] += weightArr[qId];
        }

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

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

由代码中可以发现,其实我们在寻找p或者q的标识的时候,默认情况下就需要遍历这棵树的层级,而且,除了遍历以外的事情我们都没有做,这样单纯的遍历,一定程度上浪费了一些性能,那么不妨从这里入手,我们把find方法进行一下改造。

改造的办法也很简单,就是在加权quick-union的find方法中再做一次小的变化

package com.zt;

public class PathCompressionQuickUnionUnionFind extends UnionFind {

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

    public PathCompressionQuickUnionUnionFind(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[pId] < weightArr[qId]) {
            id[pId] = qId;
            weightArr[qId] += weightArr[pId];
        } else {
            id[qId] = pId;
            weightArr[pId] += weightArr[qId];
        }

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

    @Override
    int find(int p) {
        // 沿着标识路径一路寻找,直到找到树的标识
        while (p != id[p]) {
            // 将父节点指向父节点的父节点,完成一次压缩操作
            id[p] = id[id[p]];
            p = id[p];
        }
        return p;
    }
}

样本验证

构造了一个样本数据集,来说明路径压缩的问题

-------------------------------
    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
   10    7    1
-------------------------------

未压缩路径执行结果

-------------------------------
    0    1    2    3    4    5    6    7    8    9
-------------------------------
    5    5    5    3    5    5    5    6    8    5
-------------------------------
image.png

压缩路径执行结果

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

image.png

经过进一次的优化后,可以看到并查集中的树的层级又再一次得到了进化,深度由原来的3变为了2。

更进一步

其实通过代码可以发现,我们在路径压缩的过程中,只是简单的把父节点指向了父节点的父节点,但是实际上,我们可以直接将父节点指向根节点,就可以保证每次find操作都是在O(1)时间内

代码如下

    @Override
    int find(int p) {
        if (p == id[p]) return p;
        // 递归直接找到根节点,将分量直接指向根节点,完成彻底的路径压缩.
        id[p] = find(id[p]);
        return id[p];
    }

实际上的代码变动也很简单,只是进行了一次递归,将分量指向根节点。

至此,我们完成了并查集的全部升级路线,实际上,即便是最终的路径压缩方案,也没办法完美的打到O(1)时间复杂度,因为我们仍然还有递归操作,但是这样也可以带来了更高的性能提升。

总结

其实从并查集的升级路线来看,我们每一次的升级改动并不大,只不过是根据实际的问题发生,找到了算法性能瓶颈点,然后一步步分析突破,最后可以得到一个优质的算法,而且,每一步的升级,都进行了样本的验证,通过样本示例可以佐证我们的方案是正确的。

展望

后续会进一步结合实际的应用,将并查集的思想和实际结合起来,能够学会更好的运用这种数据结构。

相关文章

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

    路径压缩并查集 并查集在经过了quick-find、quick-union、加权quick-union的演化之后,...

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

    并查集是为了解决连通性问题,首先现将并查集模型定义出来。 首先并查集中存储的是是分量,不通分量可能存在连通关系,定...

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

    quick-union样本分析 在上一次的测试验证中,可以得出quick-union相对于quick-find算法...

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

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

  • [并查集]并查集应用之省份数量

    前言 经过并查集的升级路线一二三四之后,我们现在得到了一个相对来说比较完美的并查集数据结构,从本篇开始应用这个并查...

  • markdown学习

    #并查集 ##并查集的定义 ##并查集的操作

  • 算法模板(四)并查集

    并查集 路径压缩并查集

  • 并查集入门使用

    本文参考自《算法笔记》并查集篇 并查集的定义 什么是并查集?并查集可以理解为是一种维护数据集合的结构。名字中并查集...

  • 并查集

    并查集 并查集是什么 并查集是一种用来管理元素分组情况的数据结构,并查集可以高效地进行如下操作: 查询元素a和元素...

  • 并查集练习

    并查集 参考《算法笔记》 三要素:并,查,集。 并差集与一个算法有关,kruskal算法,就可通过并查集实现。 1...

网友评论

      本文标题:[并查集]并查集的升级路线(四)

      本文链接:https://www.haomeiwen.com/subject/ejeyxltx.html