[路飞]_js中的并查集

前序

在瓦洛兰大陆上,有着两个势不两立的阵营:诺克萨斯和德玛西亚。同一个阵营中的是不能相互攻击的,所以他们PK之前,得确认一下对方的身份,避免造成乌龙事件。一开始,他们会带着同样的令牌,每次大家PK之前拿出令牌对比一下,如果是同个阵营的,大家就大事化小小事化了。这样子比较的时候是最快的,直接相互拿个令牌出来就可以,但是在合并联盟的时候比较麻烦,比如盖伦嘉文皇子他们的小团体加入了德玛西亚,那么他们这个团队里的人都得换令牌,如果赵信先加入嘉文团队,再加入德玛西亚,那么他就得换两次令牌。每次两个小团队进行联盟的时候,总有一个势力需要内部全体进行更换令牌操作,确保联盟后大家的令牌是统一的,这就是并查集中的Quick-Find方法。这种方法的好处是在确认双方是否同一阵营的时候,可以最快速的判断出来,因为同一阵营的令牌总是相同的。

可是这样子的话,阵容下面的小弟,可能一天要换一次令牌。加里奥是个暴脾气的小伙子,再经历了三天更换五次令牌后他终于忍受不了爆发了,说谁爱换谁换,反正我就拿现在这个令牌了。于是长官们商量了一下,又想出了另一个方法。就是每个人管好自己的士兵,然后PK之前问一下对方的领导是谁,如果不认识的话让领导去问他们的领导,这样子问到最高领导的时候,同一阵营的最高领导总是相同的,每个阵营中只能有一个国王。这样子小弟们终于脱离了频繁更换令牌的烦恼。每个士兵除了刚入伍的时候认准一个长官,从此以后就一直归属于长官的领导了。这就是并查集中的Quick-Union方法。这种方法的好处是在每个士兵只需要确认一次上级,在合并的时候可以直接将第二个军队的将领归纳到第一个军队的将领麾下,他部下的士兵不需要做任何改变,可以最快速的进行合并,但是在判断是否同一阵营的时候,需要从最下级一层层往上循环,问到最高级的长官也就是国王,确认双方是否同一个国王。

但是这样子的话也有坏处,就是每次PK之前得经过一轮轮询问,等一个小时后双方都问完了肚子都饿了。于是长官们又想了个办法,就是第一次询问过后,每一级往上询问后,上级反馈回来国王是谁。他们就记录下来自己的国王。那么下次PK之前只需要问一下记录下的国王,当前他是否还是最高领导即可。为什么需要这一步呢?因为我们直到阵营是可以进行合并的,合并过后双方只能有一个国王,另一方成为国王的将军。这一步在并查集中叫做路径压缩,也是最常用的Union-Find

简介

上述的场景中,在我们代码世界里可以用并查集(Union Find)来记录阵营之间的关系。并查集是一种用于管理分组的数据结构。它具备两个操作:

(1)查询元素a和元素b是否为同一组   

(2) 将元素a和b合并为同一组。

实现

QuickFind

class QuickFind {
  constructor(n) {
    // 一开始每个人的上级都是他们自己, 我们同个索引来标识上级的位置
    this.parent = new Array(n).fill(0).map((item, index) => index);
  }

  // 找到某个人的上级
  find(index) {
    return this.parent[index];
  }

  // 合并两个军队, 这里我们以军队1为主,军队2的人全部更换令牌
  merge(index1, index2) {
    // 找到军队1和军队2的令牌
    let p1 = this.find(index1), p2 = this.find(index2);

    // 把军队2的令牌全部换成军队1的
    this.parent.forEach((item, index) => {
       if (this.find(index) === p2) {
        this.parent[index] = p1;
      }
    });
  }
}
复制代码

QuickUnion

class QuickUnion {
  constructor(n) {
    // 一开始每个人的上级都是他们自己, 我们同个索引来标识上级的位置
    this.parent = new Array(n).fill(0).map((item, index) => index);
  }

  // 找到某个人的最高长官
  find(index) {
    // 最高长官意味着他是自己的上级
    if (this.parent[index] === index) return index;

    // 否则递归往上找
    return this.find(this.parent[index]);
  }

  // 合并两个军队, 这里我们以军队1为主,军队2的领导从此认了大哥
  merge(index1, index2) {
    let p1 = this.find(index1), p2 = this.find(index2);
    if (p1 !== p2) {
        this.parent[p2] = p1;
    }
  }
}
复制代码

UnionFind

class UnionFind {
  constructor(n) {
    // 一开始每个人的上级都是他们自己, 我们同个索引来标识上级的位置
    this.parent = new Array(n).fill(0).map((item, index) => index);
  }

  // 找到某个人的最高长官
  find(index) {
    // 最高长官意味着他是自己的上级
    if (this.parent[index] === index) return index;

    // 否则递归往上找

    // 路径压缩
    this.parent[index] = this.find(this.parent[index]);
    
    return this.parent[index];
  }

  // 合并两个军队, 这里我们以军队1为主,军队2的领导从此认了大哥
  merge(index1, index2) {
    let p1 = this.find(index1), p2 = this.find(index2);
    if (p1 !== p2) {
        this.parent[p2] = p1;
    }
  }
}
复制代码

代码精简

class UnionFind {
  constructor(n) {
    // 一开始每个人的上级都是他们自己, 我们同个索引来标识上级的位置
    this.parent = new Array(n).fill(0).map((item, index) => index);
  }

  // 找到某个人的最高长官
  find(index) {
    // 最高长官意味着他是自己的上级, 否则递归往上找
    return this.parent[index] = this.parent[index] === index ? index : this.find(this.parent[index]);
  }

  // 合并两个军队, 这里我们以军队1为主,军队2的领导从此认了大哥
  merge(index1, index2) {
    this.parent[this.find(index2)] = this.find(index1);
  }
}
复制代码

到这一步就以及写完了并查集结构的代码,但是这并不意味着我们就没有优化空间了。我们可以在合并操作的时候,判断两个要合并的结构中的节点数量,把节点少的树合并到节点多的树中去。不过这一步的优化效果不是很明显,而且还会增加我们的心智负担。这里我就不再往下罗列了。

Guess you like

Origin juejin.im/post/7047480642166063118