并查集基础
- 并查集是一类抽象化程度很高的数据结构
连通性问题
- 维护连通关系以及查询连通关系的这一类问题
- 举个例子,现在有两个操作
- 一个是连通两个元素,即合并两个元素所在的集合
- 一个是判断两个元素之间是否有关系,即是否属于同一个集合
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-find 算法的连通判断非常快,但是合并操作非常慢
- 思考:
- 本质上,上述问题中,只需要知道一个点与其他哪些点的颜色相同
- 而若干点的颜色可以通过间接指向同一个节点
- 合并操作时,实际上是将一颗子树作为另一棵树的子树
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
,通过路径压缩
进行优化后,依然能够很好的提升程序运行效率 - 考虑实现成本和带来的程序运行效率的提升收益,一般情况下,不需要加上
按秩合并
的代码 在数据量较大时,可以再考虑加上
按秩合并
的优化 ```javascript 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)); }
```