vb6实现union数据结构_数据结构 | 并查集 & 联合 - 查找算法

前言

  • 并查集是一种非常适用于处理 动态连通 问题的数据结构,在面试中比较冷门,建议应试者合理安排学习时间;
  • 在这篇文章里,我将梳理并查集的基本知识 & 常考题型。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

系列文章

  • 《数据结构 | 二叉树基础》
  • 《数据结构 | 并查集 & 联合 - 查找算法》

目录

68e0a72e7646c624615a474cd6a9eeb0.png

1. 定义

并查集还有多个相同的名字:不相交集合(Disjoint Sets)、合并-查找集合(Merge-find Set)、联合-查询数据结构(Union-find Data Structure)、联合-查询算法(Union-find algorithm),从这么多的相似概念中,我们可以体会到并查集解决的问题,即:处理不相交集合的合并和查询。

具体来说,用于一对元素是否相连。因为元素之间的关系并不是一开始就可以确定的,所以需要一边查询一边连接(合并),这种问题叫做动态连通性问题。举个例子,990. Satisfiability of Equality Equations 等式方程的可满足性

引用自 LeetCode


2. 并查集的实现

并查集在底层实现可以是数组,也可以是散列表,不管使用什么底层实现,关键在于能表示一个对应关系,即:key 或下标表示了节点本身,而 value 表示顶点的父亲节点,初始化时指向自己

一般来说,当节点总个数固定不变 / 已知时,使用数组,否则使用散列表。例如在 990. Satisfiability of Equality Equations 等式方程的可满足性 这道题中,节点是已知的 26 个字母,此时使用数组即可;在 684. Redundant Connection 冗余连接 这道题中,节点个数是未知的,此时使用散列表更合适。

基于数组class UnionFind(n: Int) {    1. 初始时,节点指向自己    val parent = IntArray(n) { it }     ...}-------------------------------------------------------基于散列表class UnionFind() {    val parent = HashMap()        fun find(x: Int): Int {        1. 首次查询时,添加到散列表,并指向自己        if (null == parent[x]) {            parent[x] = x        }        var key = x        while (parent[key] != key) {            parent[key] = parent[parent[key]]!!            key = parent[key]!!        }        return key    }}

提示
这里为不熟悉 Kotlin 的同学解释一下,IntArray(n) { it }其实是IntArray(n){ index -> index },即:创建了一个数组,数组每个位置上的值为下标的值。

83aa7d07cee11dcf391b9789d7fd288f.png

并查集的物理与逻辑实现

可以看到,并查集在物理实现上基于数组或散列表,从逻辑上就是若干棵树组成的森林,每个节点都持有父节点的引用。


3. 并查集的两个操作

并查集使用根节点来代表一个集合,这种方法叫做 代表元法,根节点就是代表元。在此基础上,并查集实现了两个基本操作:合并 & 查询

  • 合并操作
    合并操作就是将一个节点的的父节点指向另一个的根节点
  • 查询操作
    查询操作就是查询节点的根节点,如果两个节点的根节点相同,那么表示在一个集合中(相连)。

例如,使用Union(x,y)来合并两个节点,而Find(x)查询元素的根节点(代表元)。下面是一个最基本的并查集实现:

private class UnionFind(n: Int) {    1. 初始时,节点指向自己    val parent = IntArray(n) { it }    2. 查询    fun find(x: Int): Int {        var key = x        while (key != parent[key]) {            key = parent[key]        }        return key    }        3. 合并    fun union(x: Int, y: Int) {        val rootX = find(x)        val rootY = find(y)        parent[rootY] = rootX     }    4. 判断连通性    fun isConnected(x: Int, y: Int): Boolean {        return find(x) == find(y)    }}
e8417698bfa2660ef793d13ee5fb3f5e.png

并查集的合并操作


4. 连通问题 & 路径问题

并查集专注于解决连通问题,即两个元素是否相连,不关心元素之间的路径 & 距离;而路径问题往往需要找出连接两个元素的路径甚至是最短路径,这就超出了并查集的处理范围。

举个例子,给定一系列航班信息,北京——上海,上海——苏州,苏州——杭州,杭州——北京,问是否存在北京到苏州的路径,这就是连通问题,而问北京到苏州的最短路径,这就是路径问题。

关于并查集的 连通分量,表示的是整个并查集中独立的集合(或树)的个数。例如:

private class UnionFind(n: Int) {    1. 初始时,节点指向自己    val parent = IntArray(n) { it }    2. 连通分量计数    var count = n        3. 合并    fun union(x: Int, y: Int) {        val rootX = find(x)        val rootY = find(y)        if(rootX == rootY){            return        }        // 合并后,连通分量减一        parent[rootY] = rootX         count --     }    ...}
d2e2fbf20fe62c1bc102635825ca4fd8.png

连通分量就是集合个数


5. 并查集的优化

前面说到并查集逻辑上是树的数据结构,既然是树的结构,性能就与树的高度有关。极端情况下,会出现树的高度为 n 的情况(例如 union(6,7).union(5,6)、union(4,5,)、..),此时,查询的复杂度将退化到O(n)

在并查集里,有两类针对树高度的优化:路径压缩 & 按合并:

5.1 路径压缩(Path compression)

指在查询的过程中,更改父节点的引用使得树的高度更低,具体分为 隔代压缩 & 完全压缩

  • 隔代压缩:将父节点指向父节点的父节点
1. 非递归写法fun find(x: Int): Int {    var key = x    while (parent[key] != key) {        parent[key] = parent[parent[key]] // 关键        key = parent[key]    }    return key}-------------------------------------------------------2. 递归写法fun find(x: Int): Int {    var key = x    if (parent[key] != key) {        parent[key] = find(parent[key])    }    return parent[key] // 关键}

提示
这里为不熟悉 Kotlin 的同学解释一下,为什么要加 var key = x 呢?因为 Kotlin 中函数形参是不可变变量。

  • 完全压缩:直接将父节点指向根节点
75f0c6c161682aeb74ba2e5c84271314.png

隔代压缩 & 完全压缩

5.2 按秩(Rank)合并

指在合并的过程中,将高度更低的树根节点指向高度更高的根节点,避免合并以后树的高度增加。为了表示树的高度,使用 rank 数组,表示以第 i 个节点为根的树的高度。

class UnionFind(n: Int) {    1. 根节点的高度    private val rank = IntArray(n) { 1 }    2. 查询(未使用路径压缩)    fun find(x: Int): Int {        var key = x        while (key != parent[key]) {            key = parent[key]        }        return key    }    3. 按秩合并    fun union(key1: Int, key2: Int) {        val root1 = find(key1)        val root2 = find(key2)        if(rank[root1] > rank[root2]){            parent[root2] = root1        }else if(rank[root2] > rank[root1]){            parent[root1] = root2        }else{            parent[root1] = root2            rank[root1] ++            //  或            //  parent[root2] = root1            //  rank[root2] ++        }    }}
717e87778387e3d19c7860baa2119ea4.png

按秩合并

5.3 小结

需要记住的是:只使用一种优化方式,查询的时间复杂度是

e7c2519751ed073bcbd6f5b3362df019.png

,如果同时使用路径压缩和按秩合并,查询的时间复杂度接近

54f192feab6ba1a4448dbf6533d93b6d.png

(反阿克德函数),可以说是相当优秀了。通常情况下,路径压缩和按秩合并使用一个即可。


6. 总结

并查集比较冷门,建议应试者合理安排学习时间,在优化并查集时,一般路径压缩和按秩合并使用一个即可。隔代压缩实现简单,也很好记,建议使用。


原文链接:https://www.jianshu.com/p/d4ce8e21027c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值