
并查集
并查集是一种树形的数据结构,用于处理一些不相交集合的合并及查询问题,在使用中通常以森林来表示。
并查集的思想是用一个数组来表示整片森林,树的根节点唯一标识了一个集合,我们只要找到了某个元素的树根,就能知道它在哪个集合里。
通常情况下,用集合中的某个元素来代表这个集合,该元素称为集合的代表元。
一个集合内的所有元素组织成以代表元为根的树形结构。
对于每一个元素 parent[x]指向x在树形结构上的父亲节点。如果x是根节点,则令parent[x] = x。
对于查找操作,假设需要确定x所在的的集合,也就是确定集合的代表元。可以沿着parent[x]不断在树形结构中向上移动,直到到达根节点。
例如在下面的一组元素中,每个元素代表一个集合,用 parent 表示当前元素指向的父节点,每个元素指向自己,所以它们都是这个集合中的根节点。
并查集解决的问题
并查集用在一些有 N 个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,期间要反复查找一个元素在哪个集合中。这个过程看似并不复杂,但数据量极大,若用其它的数据结构来描述的话,往往在空间上过大,计算机无法承受,也无法在短时间内计算出结果,所以只能用并查集来处理。
初始化
class UnionFind {constructor(n) {this.parent = new Array(n).fill(0).map((value, index) => index);this.rank = new Array(n).fill(1);this.setCount = n;}}
定义一个 UnionFind 类,在该类的构造函数 constructor 中分别定义 parent、rank、setCount 三个属性。其中:
- parent:表示元素所指向的父节点,那么 parent[i] 表示的是第 i 个元素所指向的父节点
- rank:表示树的层数,那么 rank[i] 表示的是以 i 为根的集合所表示的树的层数
- setCount:表示节点的个数
在初始化时,每个 parent[i] 指向自己,表示每一个元素自己自成一个集合,此时 i 为根节点。每个以 i 为根的集合所表示的树的层数为 1 。
查找
// 查找元素 index 所对应的集合编号findSet(index) {// 不断去查询自己的父节点,直至根节点// 根节点的标志是父节点就是本身 parent[index] == indexif (this.parent[index] != index) {// 递归获取子节点的父节点this.parent[index] = this.findSet(this.parent[index]);}return this.parent[index];}
在查找某个元素所对应的集合时,通过递归的方式一层一层地访问父节点,直至根节点 (根节点的标志就是父节点是本身) 。
合并
unite(index1, index2) {let root1 = this.findSet(index1), root2 = this.findSet(index2);if (root1 != root2) {if (this.rank[root1] < this.rank[root2]) {[root1, root2] = [root2, root1];}this.parent[root2] = root1;this.rank[root1] += this.rank[root2];this.setCount--;}}
合并两个元素所属的集合,分别查询两个元素的根节点,使其中一个根节点指向另外一个根节点,两个集合就合并了。这个操作是 O(h) 的时间复杂度,其中 h 为树的高度。
在合并两个元素所属的集合时,更为准确的一种作法是根据两个集合的层数,具体判断根节点的指向,层数少的集合根节点指向层数多的集合根节点,这就可以确保合并后的集合所表示的树的层数是最少的。
例如在下图所表示的集合中,将以 7 为根节点的集合的根节点指向以 8 为根节点的集合:
合并后的集合如下图,其所表示的树的层数是最少的:
判断两个元素是否属于同一个集合
connected(index1, index2) {let root1 = this.findSet(index1), root2 = this.findSet(index2);return root1 == root2;}
断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。这个操作的事件复杂度为 O(h),其中 h 为树的高度。
路径压缩
并查集里的 findSet 方法里可以进行路径压缩,是为了更快速地查找一个节点的根节点。对于一个集合树来说,它的根节点下面可以依附着许多的节点,因此,我们可以尝试在查找的过程中,从底向上,如果此时访问的节点不是根节点的话,那么我们可以把这个节点尽量的往上挪一挪,减少树的层数,这个过程就叫做路径压缩。
如下图中,findSet(5) 的过程就可以做路径压缩,让树的层数更少。
节点 5 往上寻找根节点时,把节点5指向其原来父节点的父节点,即指向节点3,树的层数就减少了一层:
节点 3 继续往上寻找,也不是根节点,那么把节点3 指向其原来父节点的父节点,即指向节点1,树的层数减少了一层,此时节点 1 是根节点 (根节点的标志是父节点是其本身),在 findSet 中返回根节点。
上述的路径压缩不是最优的方式,我们可以把最初的树压缩成下图所示,树的层数是最少的:
这个过程的 findSet 代码为:
findSet(index) {// 路径压缩if (this.parent[index] !== index) {// 使用递归获取子节点的父节点this.parent[index] = this.findSet(this.parent[index])}// 返回根节点return this.parent[index]}
完整代码
class UnionFind {constructor(n) {// 元素所指向的父节点,parent[i] 表示第 i 个元素所指向的父节点// 初始化时, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合this.parent = new Array(n).fill(0).map((value, index) => index);// 树的层数,rank[i] 表示以 i 为根的集合所表示的树的层数this.rank = new Array(n).fill(1);// 节点的个数this.setCount = n;}// 查找过程,查找元素 index 所在集合的编号(查找树的根节点)findSet(index) {// 不断去查询自己的父节点,直至根节点// 根节点的标志是父节点就是本身 parent[index] == indexif (this.parent[index] != index) {// 递归获取节点的父节点this.parent[index] = this.findSet(this.parent[index]);}// 返回根节点return this.parent[index];}// 合并两个集合unite(index1, index2) {let root1 = this.findSet(index1);let root2 = this.findSet(index2);// 根节点不一样,是两个不同的集合(两棵不同的树)if (root1 != root2) {// 根据树的层数合并集合//if (this.rank[root1] < this.rank[root2]) {// 这个判断如果 root2 所在树的层数 大于 root1,就交换两个父节点,这样始终让 root1 为父节点[root1, root2] = [root2, root1];}// 将层数多的集合合并到集合少的集合this.parent[root2] = root1;this.rank[root1] += this.rank[root2];this.setCount--;}}getCount() {return this.setCount;}connected(index1, index2) {let root1 = this.findSet(index1);let root2 = this.findSet(index2);return root1 == root2;}}
