并查集定义
并查集是一种用来解决 连通性
的数据结构,抽象的方向不同会导致实现方式的不同。
我们也可以用并查集来表示集合的关系。
1.快速查找(quick-find)
如图所示,我们可以通过给元素加颜色来表示其所属集合,当然颜色只是一种比喻,实际上可以用数字或者其他字符来表示
这种并查集之所以能快速查找是因为查找方法的时间复杂度是O(1)
但是也有一个缺点就是合并比较慢,其合并方法的时间复杂度为O(n)
//* 1.quick-find 快速查找(合并慢)
//* 将属于同一集合的节点标记为同一颜色
class Union_quick_find {
private readonly colors: number[]
constructor(n: number) {
this.colors = new Array(n).fill(0).map((v, i) => i)
}
find(index: number) {
return this.colors[index]
}
merge(a: number, b: number) {
const cb = this.colors[b]
// 遍历所有元素,将属于b集合的元素归于a集合的名下
for (let i = 0; i < this.colors.length; i++) {
if (this.colors[i] === cb) this.colors[i] = this.colors[a]
}
}
}
从代码实现层面来看,我们能获得的是一个元素的所属 集合
2.快速合并 (quick-union)
顾名思义,这种并查集在合并操作时效率非常高,而查找的效率在一般的实现下是比较不稳定的,当然后面会有相应的优化方法,先介绍快速合并的并查集如何实现
在这种并查集的实现中,我们用一棵树来表示一个集合,用根节点表示一棵树(也就是一个集合)
初始化时,每个节点就是一棵树(不同树代表不同的集合)
//* 2.quick-union 快速合并
//* 将连通关系转换为树形结构,通过递归的方式快速判定
//* 老大通过树的根节点来比较
class Union_quick_union {
private readonly boss: number[]
constructor(n: number) {
this.boss = new Array(n).fill(0).map((v, i) => i)
}
find(index: number): number {
if (this.boss[index] === index) return index
return this.find(this.boss[index])
}
merge(a: number, b: number) {
const aBoss = this.find(a), bBoss = this.find(b)
if (aBoss === bBoss) return
//! 这里没有考虑到两颗树的情况,因此在极端情况下查找效率会非常低
this.boss[aBoss] = bBoss
}
}
从以上代码可以发现,查找方法的效率由树高和节点的位置决定,当查找的节点所在树越高、越处在底层,查找效率越慢
同时,上述代码在合并的时候也欠考虑,比如说合并的树比合并前的树高还要高,这样的结果是增加了查找的负担
那是不是就意味着矮的树合并到高的树上就行了呢?其实,我们应该根据树的平均查找次数来判断才是最公正的
树 的 平 均 查 找 次 数 = ∑ 节 点 的 查 找 次 数 节 点 数 树的平均查找次数=\frac{\sum{节点的查找次数}}{节点数} 树的平均查找次数=节点数∑节点的查找次数
节点的查找次数表示一棵树从根节点到目标节点需要走的步数
令 有 a 、 b 两 棵 树 , a 树 有 s a 个 节 点 , 总 共 有 l a 次 查 找 次 数 b 树 有 s b 个 节 点 , 总 共 有 l b 次 查 找 次 数 以 a 为 根 的 合 并 树 的 平 均 查 找 次 数 = l a + l b + s b s a + s b 以 b 为 根 的 合 并 树 的 平 均 查 找 次 数 = l a + l b + s a s a + s b 令有a、b两棵树,a树有s_a个节点,总共有l_a次查找次数 \\ b树有s_b个节点,总共有l_b次查找次数 \\ 以a为根的合并树的平均查找次数=\frac{l_a+l_b+s_b}{s_a+s_b} \\ 以b为根的合并树的平均查找次数=\frac{l_a+l_b+s_a}{s_a+s_b} 令有a、b两棵树,a树有sa个节点,总共有la次查找次数b树有sb个节点,总共有lb次查找次数以a为根的合并树的平均查找次数=sa+sbla+lb+sb以b为根的合并树的平均查找次数=sa+sbla+lb+sa
从以上推导出来的两分式可以看出,节点数大的树的根作为合并树的根的话,合并树的查找次数越少
通俗地说就是节点少的当儿子,因此也有了以下的优化代码
//* 3.weighted-quick-union 加权快速合并
//* 通过权重考虑平均查找次数,对合并过程进行优化
//* 权重以节点数量来衡量
class Union_weighted_quick_union {
private readonly boss: number[]
private readonly size: number[]
constructor(n: number) {
this.boss = new Array(n).fill(0).map((val, i) => i)
this.size = new Array(n).fill(0).map(_ => 1)
}
find(index: number): number