image.png

并查集

并查集是一种树形的数据结构,用于处理一些不相交集合的合并及查询问题,在使用中通常以森林来表示。

并查集的思想是用一个数组来表示整片森林,树的根节点唯一标识了一个集合,我们只要找到了某个元素的树根,就能知道它在哪个集合里。

通常情况下,用集合中的某个元素来代表这个集合,该元素称为集合的代表元。
一个集合内的所有元素组织成以代表元为根的树形结构。
对于每一个元素 parent[x]指向x在树形结构上的父亲节点。如果x是根节点,则令parent[x] = x。
对于查找操作,假设需要确定x所在的的集合,也就是确定集合的代表元。可以沿着parent[x]不断在树形结构中向上移动,直到到达根节点。

例如在下面的一组元素中,每个元素代表一个集合,用 parent 表示当前元素指向的父节点,每个元素指向自己,所以它们都是这个集合中的根节点。
image.png

并查集解决的问题

并查集用在一些有 N 个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,期间要反复查找一个元素在哪个集合中。这个过程看似并不复杂,但数据量极大,若用其它的数据结构来描述的话,往往在空间上过大,计算机无法承受,也无法在短时间内计算出结果,所以只能用并查集来处理。

初始化

  1. class UnionFind {
  2. constructor(n) {
  3. this.parent = new Array(n).fill(0).map((value, index) => index);
  4. this.rank = new Array(n).fill(1);
  5. this.setCount = n;
  6. }
  7. }

定义一个 UnionFind 类,在该类的构造函数 constructor 中分别定义 parent、rank、setCount 三个属性。其中:

  • parent:表示元素所指向的父节点,那么 parent[i] 表示的是第 i 个元素所指向的父节点
  • rank:表示树的层数,那么 rank[i] 表示的是以 i 为根的集合所表示的树的层数
  • setCount:表示节点的个数

在初始化时,每个 parent[i] 指向自己,表示每一个元素自己自成一个集合,此时 i 为根节点。每个以 i 为根的集合所表示的树的层数为 1 。

查找

  1. // 查找元素 index 所对应的集合编号
  2. findSet(index) {
  3. // 不断去查询自己的父节点,直至根节点
  4. // 根节点的标志是父节点就是本身 parent[index] == index
  5. if (this.parent[index] != index) {
  6. // 递归获取子节点的父节点
  7. this.parent[index] = this.findSet(this.parent[index]);
  8. }
  9. return this.parent[index];
  10. }

在查找某个元素所对应的集合时,通过递归的方式一层一层地访问父节点,直至根节点 (根节点的标志就是父节点是本身) 。

合并

  1. unite(index1, index2) {
  2. let root1 = this.findSet(index1), root2 = this.findSet(index2);
  3. if (root1 != root2) {
  4. if (this.rank[root1] < this.rank[root2]) {
  5. [root1, root2] = [root2, root1];
  6. }
  7. this.parent[root2] = root1;
  8. this.rank[root1] += this.rank[root2];
  9. this.setCount--;
  10. }
  11. }

合并两个元素所属的集合,分别查询两个元素的根节点,使其中一个根节点指向另外一个根节点,两个集合就合并了。这个操作是 O(h) 的时间复杂度,其中 h 为树的高度。

在合并两个元素所属的集合时,更为准确的一种作法是根据两个集合的层数,具体判断根节点的指向,层数少的集合根节点指向层数多的集合根节点,这就可以确保合并后的集合所表示的树的层数是最少的。

例如在下图所表示的集合中,将以 7 为根节点的集合的根节点指向以 8 为根节点的集合:
image.png
合并后的集合如下图,其所表示的树的层数是最少的:
image.png

判断两个元素是否属于同一个集合

  1. connected(index1, index2) {
  2. let root1 = this.findSet(index1), root2 = this.findSet(index2);
  3. return root1 == root2;
  4. }

断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。这个操作的事件复杂度为 O(h),其中 h 为树的高度。

路径压缩

并查集里的 findSet 方法里可以进行路径压缩,是为了更快速地查找一个节点的根节点。对于一个集合树来说,它的根节点下面可以依附着许多的节点,因此,我们可以尝试在查找的过程中,从底向上,如果此时访问的节点不是根节点的话,那么我们可以把这个节点尽量的往上挪一挪,减少树的层数,这个过程就叫做路径压缩。

如下图中,findSet(5) 的过程就可以做路径压缩,让树的层数更少。
image.png
节点 5 往上寻找根节点时,把节点5指向其原来父节点的父节点,即指向节点3,树的层数就减少了一层:
image.png
节点 3 继续往上寻找,也不是根节点,那么把节点3 指向其原来父节点的父节点,即指向节点1,树的层数减少了一层,此时节点 1 是根节点 (根节点的标志是父节点是其本身),在 findSet 中返回根节点。
image.png

上述的路径压缩不是最优的方式,我们可以把最初的树压缩成下图所示,树的层数是最少的:
image.png
这个过程的 findSet 代码为:

  1. findSet(index) {
  2. // 路径压缩
  3. if (this.parent[index] !== index) {
  4. // 使用递归获取子节点的父节点
  5. this.parent[index] = this.findSet(this.parent[index])
  6. }
  7. // 返回根节点
  8. return this.parent[index]
  9. }

完整代码

  1. class UnionFind {
  2. constructor(n) {
  3. // 元素所指向的父节点,parent[i] 表示第 i 个元素所指向的父节点
  4. // 初始化时, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
  5. this.parent = new Array(n).fill(0).map((value, index) => index);
  6. // 树的层数,rank[i] 表示以 i 为根的集合所表示的树的层数
  7. this.rank = new Array(n).fill(1);
  8. // 节点的个数
  9. this.setCount = n;
  10. }
  11. // 查找过程,查找元素 index 所在集合的编号(查找树的根节点)
  12. findSet(index) {
  13. // 不断去查询自己的父节点,直至根节点
  14. // 根节点的标志是父节点就是本身 parent[index] == index
  15. if (this.parent[index] != index) {
  16. // 递归获取节点的父节点
  17. this.parent[index] = this.findSet(this.parent[index]);
  18. }
  19. // 返回根节点
  20. return this.parent[index];
  21. }
  22. // 合并两个集合
  23. unite(index1, index2) {
  24. let root1 = this.findSet(index1);
  25. let root2 = this.findSet(index2);
  26. // 根节点不一样,是两个不同的集合(两棵不同的树)
  27. if (root1 != root2) {
  28. // 根据树的层数合并集合
  29. //
  30. if (this.rank[root1] < this.rank[root2]) {
  31. // 这个判断如果 root2 所在树的层数 大于 root1,就交换两个父节点,这样始终让 root1 为父节点
  32. [root1, root2] = [root2, root1];
  33. }
  34. // 将层数多的集合合并到集合少的集合
  35. this.parent[root2] = root1;
  36. this.rank[root1] += this.rank[root2];
  37. this.setCount--;
  38. }
  39. }
  40. getCount() {
  41. return this.setCount;
  42. }
  43. connected(index1, index2) {
  44. let root1 = this.findSet(index1);
  45. let root2 = this.findSet(index2);
  46. return root1 == root2;
  47. }
  48. }