「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」
什么是并查集
- 一句话概括就是:并查集是一类抽象化程度很高的数据结构
连通性问题
- 适合维护连通关系以及查询连通关系的这一类问题
- 举个例子,现在有两个操作
- 一个是连通两个元素,即合并两个元素所在的集合
- 一个是判断两个元素之间是否有关系,即是否属于同一个集合
Quick-Find 算法
- 思路:用染色法来标记,同颜色的元素为一个集合
- 维护连通关系:合并两个集合,代表需要将两个集合的所有元素,染成统一的颜色
- 查询联通关系:判断元素是否属于某个集合,代表需要对比,元素的颜色与集合的颜色是否相同
- 实现:
/**
*
* 染色法表示集合关系,两个元素的颜色相同时,表示这两个元素属于同一个集合
*
* 查找操作复杂度为 O(1)
* 合并操作复杂度为 O(n)
*/
class QuickFind {
constructor(n) {
// 初始化元素对应的颜色集合
this.color = new Array(n);
// 初始化每个元素的颜色,每个元素的颜色都不一样
for (let i = 0; i < n; i++) {
this.color[i] = i;
}
}
/**
* 查询 元素 所在的集合
* 即,查找元素的颜色
*/
find = (x) => {
return this.color[x];
};
/**
* 连通 a 与 b
* 意味着,a 与 b 所在的集合,将被合并为一个集合
* 即,将两个集合染成统一的颜色
*
* 同一种颜色的元素,表示是在同一个集合中
*/
merge = (a, b) => {
if (this.find(a) === this.find(b)) return;
const color_a = this.find(a);
const color_b = this.find(b);
// 遍历所有集合中的元素颜色,将颜色与 b 相同的元素,染成 a 的颜色
for (let i = 0; i < n; i++) {
if (this.find(i) === color_b) {
this.color[i] = color_a;
}
}
};
}
- 小结:
- quick-find 算法的连通判断非常快,但是合并操作非常慢
- 连通判断时间复杂度为 O(1)
- 合并操作时间复杂度为 O(n)
- 思考:
- 本质上,上述问题中,只需要知道一个点与其他哪些点的颜色相同
- 而若干点的颜色可以通过间接指向同一个节点
- 合并操作时,实际上是将一颗子树作为另一棵树的子树
Quick-Union 算法
- 思路:树形结构表示集合关系,两个元素的根节点相同时,表示元素为同一个集合
- 维护连通关系:合并两个集合,代表需要将两个集合的根节点统一即可
- 查询联通关系:判断元素是否属于某个集合,代表需要对比,元素的根节点与集合的根节点是否相同
- 实现:
class QuickUnion {
constructor(n) {
// 初始化一个 元素对应的 root 的集合
this.root = new Array(n);
// 初始化每个元素的 root 为自己
for (let i = 0; i < n; i++) {
this.root[n] = i;
}
}
// 查询元素的 root
find = (x) => {
if (this.root[x] === x) return x;
return this.find(this.root[x]);
};
// 连通 a 和 b 两个元素
// 意味着将 a 和 b 所在的集合,合并为一个集合
// 直接将 a 集合的根节点,挂在 b 集合根结点上即可
merge = (a, b) => {
const root_a = this.find(a);
const root_b = this.find(b);
if (root_a === root_b) return;
this.root[a] = root_b;
};
}
- 小结:
- 连通判断 与 合并操作 的复杂度与树高有关,树高越深,复杂度越大
- 思考:
- 极端情况下,树会变成一条链表
- 合并操作时,将节点数量多的树,接到节点数量少的树上面,会导致树高变深
- 合并操作时,将树高较深的树,接到数高较浅的上面,会导致树高变深
Weighted Qiuck-Union 算法
- 很显然,要优化上面的 Quick-Union 算法,需要优化合并操作,使得合并后的树的每个节点查找跟节点的次数变少
- 优化策略为:
- 合并树时,将节点较少的树作为子树,挂到节点数量较多的树的根节点上
- 这种方法叫:
按秩合并
class UnionSet {
// n 为元素个数,这些元素的编号为 0 ~ n-1
constructor(n) {
// 每个元素对应的 所在集合的 根节点
this.root = new Array(n);
// 每个元素对应的 所在集合的 元素数量
this.size = new Array(n);
// 初始化时,集合中每个元素的根节点为自己,所以每个元素所在集合的元素数量为 1
for (let i = 0; i < n; i++) {
this.root[i] = i;
this.size[i] = 1;
}
}
// 查找 x 的根节点
find = (x) => {
if (this.root[x] === x) return x;
return this.find(this.root[x]);
};
// 连通 a, b 两点
// 也就是合并 a, b 所在的两个集合
// 也就是 统一 这两个集合的根节点
merge = (a, b) => {
let root_a = this.find(a);
let root_b = this.find(b);
// 根节点相同,表示两个元素已经是属于同一个集合了
if (root_a === root_b) return;
// 元素对应的集合的元素个数较大者,其根元素应作为合并后的集合的根节点
if (this.size[root_a] > this.size[root_b]) {
this.root[root_b] = root_a;
this.size[root_a] += this.size[root_b];
} else {
this.root[root_a] = root_b;
this.size[root_b] += this.size[root_a];
}
};
}
Weighted Qiuck-Union With Path Compression
- 带路径压缩的 Weighted Qiuck-Union 算法
- 理想情况下,元素都直接挂在根节点上,那么在查找该元素的根节点时,就会非常快
- 优化策略为:
- 每次查找到元素的根结点时,直接将该元素挂在其根节点上
- 再次查找该元素根节点时就会非常快
- 这种方法叫:
路径压缩
class UnionSet {
// n 为元素个数,这些元素的编号为 0 ~ n-1
constructor(n) {
// 每个元素对应的 所在集合的 根节点
this.root = new Array(n);
// 每个元素对应的 所在集合的 元素数量
this.size = new Array(n);
// 初始化时,集合中每个元素的根节点为自己,所以每个元素所在集合的元素数量为 1
for (let i = 0; i < n; i++) {
this.root[i] = i;
this.size[i] = 1;
}
}
// // 查找 x 的根节点
// find = (x) => {
// if (this.root[x] === x) return x;
// return this.find(this.root[x]);
// };
/**
*
* 路径压缩优化
*
* 每次查找到元素的 root 之后,直接将元素挂载到 root 上
* 下次查找该元素的 root 时,只需要查找一次即可
*
*/
find = (x) => {
if (this.root[x] === x) return x;
let root = this.find(this.root[x]);
this.root[x] = root;
return root;
};
// 连通 a, b 两点
// 也就是合并 a, b 所在的两个集合
// 也就是 统一 这两个集合的根节点
//
//
merge = (a, b) => {
let root_a = this.find(a);
let root_b = this.find(b);
// 根节点相同,表示两个元素已经是属于同一个集合了
if (root_a === root_b) return;
// 元素对应的集合的元素个数较大者,其根元素应作为合并后的集合的根节点
if (this.size[root_a] > this.size[root_b]) {
this.root[root_b] = root_a;
this.size[root_a] += this.size[root_b];
} else {
this.root[root_a] = root_b;
this.size[root_b] += this.size[root_a];
}
};
}
时间复杂度小结
| Algorithm | Constructor | Union | Find |
|---|---|---|---|
| Quick-Find | N | N | 1 |
| Quick-Union | N | Tree Height | Tree Height |
| Weighted Quick-Union | N | LogN | LogN |
| Weighted Quick-Union With Path Compression | N | Very near to 1 (amortized) | Very near to 1 (amortized) |
带路径优化的并查集算法代码模版
- 上述代码,最耗时的操作为
find,通过路径压缩进行优化后,依然能够很好的提升程序运行效率 - 考虑实现成本和带来的程序运行效率的提升收益,一般情况下,不需要加上
按秩合并的代码 - 在数据量较大时,可以再考虑加上
按秩合并的优化
class UnionSet {
// n 为元素个数,这些元素的编号为 0 ~ n-1
constructor(n) {
// 每个元素对应的 所在集合的 根节点
this.root = new Array(n);
// 初始化时,集合中每个元素的根节点为自己,所以每个元素所在集合的元素数量为 1
for (let i = 0; i < n; i++) {
this.root[i] = i;
}
}
find = (x) => (this.root[x] = this.root[x] === x ? x : this.find[this.root[x]]);
// 不需要 按秩合并,直接都挂在 b 的根节点上面
merge = (a, b) => (this.root[this.find(a)] = this.find(b));
}
最后
- 并查集的分享就到这里了,欢迎大家在评论区里面讨论自己的理解 👏。
- 如果觉得文章写的还不错的话,希望大家不要吝惜点赞,大家的鼓励是我分享的最大动力 🥰