Kiner算法刷题记(六):并查集与连通性问题(手撕算法篇)

系列文章导引

开源项目

本系列所有文章都将会收录到GitHub中统一收藏与管理,欢迎ISSUEStar

GitHub传送门:Kiner算法算题记

547. 省份数量
解题思路

这道题很明显就是连通性问题,一遇到连通性问题,我们第一时间就要想到使用并查集解决,我们使用并查集将所有属于同一个省份的城市放到一个集合中后,我们只需要看一下集合数量便知道能分成几个省份了。

在这里,我们的集合数量其实就是我们并查集中根节点的数量。

代码实现
/*
 * @lc app=leetcode.cn id=547 lang=typescript
 *
 * [547] 省份数量
 */

// @lc code=start

class UnionSet {
    // 用于存储当前节点的父节点序号
    // 虽然并查集逻辑上是树形结构,但我们实际实现时无需定义一个树形结构,因为我们想要知道的仅仅是当前节点的父级到底是谁就足够了,因此,我们使用数组来进行存储,数组的索引为当前节点的索引,值为当前节点父节点的索引
  private boss: number[];
  constructor(private n: number) {
        // 为了防止部分场景序号是从1开始的,因此初始化数组长度为n+1
      this.boss = new Array<number>(n+1);
        // 初始设置所有节点的父节点都为它本身
      for(let i=0;i<=n;i++) {
          this.boss[i] = i;
      }
  }
  // 使用路径压缩算法实现并查集的查找操作
    // 其实原理就是每查找到一个节点,我们就把节点的父级改成根节点,这样我们下次查找相同节点时,效率便可显著提升
    // 判断是否为根节点的依据:如果当前节点就等于他的根节点,即:this.boss[x]===x,就说明这个节点就是根节点
  get(x: number): number {
      return (this.boss[x] = (this.boss[x] === x ? x : this.get(this.boss[x])));
  }
      // 由于权重并查集的算法相较于路径压缩的算法带来的提升实际并不大,因为我们进行路径压缩时实际就已经极大的减少了树的层级了。因此,此处没有使用权重并查集的算法实现,直接把a树挂在b树上
  merge(a: number, b: number): void {
      this.boss[this.get(a)] = this.get(b);
  }
      // [debug] 用于调试
  output(): void {
      console.log(this.boss);
  }
}

function findCircleNum(isConnected: number[][]): number {
    const size = isConnected.length;
    // 实例化一个并查集
    const u: UnionSet = new UnionSet(size);
    // 循环遍历二位矩阵,如果如果城市i与城市j相邻,则将他们合并到一个集合当中
    for(let i=0;i<size;i++) {
        for(let j=0;j<i;j++) {
            if(isConnected[i][j]) u.merge(i, j);
        }
    }
    // 合并完之后,只要把根节点数量计算出来就是省份数量了
    let res = 0;
    for(let i=0;i<size;i++) {
        // 根节点与就是他自己的节点就是集合的根节点
        if(u.get(i) === i) res+=1;
    }

    return res;
};
// @lc code=end


200. 岛屿数量
解题思路

这题也是一个典型的连通性问题,只需要将所有的陆地按照期连通性链接起来,集合数量就是岛屿的数量。不过这一题有一个编程小技巧需要注意,那就是将一个二维数组的索引转换为一维的编号,这样才能使用并查集进行连通性处理

代码实现
/*
 * @lc app=leetcode.cn id=200 lang=typescript
 *
 * [200] 岛屿数量
 */

// @lc code=start
class UnionSet {
    // 用于存储当前节点的父节点序号
    // 虽然并查集逻辑上是树形结构,但我们实际实现时无需定义一个树形结构,因为我们想要知道的仅仅是当前节点的父级到底是谁就足够了,因此,我们使用数组来进行存储,数组的索引为当前节点的索引,值为当前节点父节点的索引
  private boss: number[];
  constructor(private n: number) {
        // 为了防止部分场景序号是从1开始的,因此初始化数组长度为n+1
      this.boss = new Array<number>(n+1);
        // 初始设置所有节点的父节点都为它本身
      for(let i=0;i<=n;i++) {
          this.boss[i] = i;
      }
  }
  // 使用路径压缩算法实现并查集的查找操作
    // 其实原理就是每查找到一个节点,我们就把节点的父级改成根节点,这样我们下次查找相同节点时,效率便可显著提升
    // 判断是否为根节点的依据:如果当前节点就等于他的根节点,即:this.boss[x]===x,就说明这个节点就是根节点
  get(x: number): number {
      return (this.boss[x] = (this.boss[x] === x ? x : this.get(this.boss[x])));
  }
      // 由于权重并查集的算法相较于路径压缩的算法带来的提升实际并不大,因为我们进行路径压缩时实际就已经极大的减少了树的层级了。因此,此处没有使用权重并查集的算法实现,直接把a树挂在b树上
  merge(a: number, b: number): void {
      this.boss[this.get(a)] = this.get(b);
  }
      // [debug] 用于调试
  output(): void {
      console.log(this.boss);
  }
}
// 用于将二维数组的索引转换成一维的编号,如:
// [
//      [1,2,3],
//      [4,5,6]
// ]
// 上述每个元素的一维索引为:
// 0,1,2,3,4,5
function idx(i: number, j: number, col: number): number{
    return i * col + j;
}

function numIslands(grid: string[][]): number {
    const row = grid.length;
    const col = grid[0].length;
    // 并查集中总共存放row*col个节点
    const u: UnionSet = new UnionSet(row * col);

    // 遍历每一个节点
    for (let i=0;i<row;i++) {
        for(let j=0;j<col;j++) {
            // 如果当前节点是水则直接进入下一次循环
            if(grid[i][j] === '0') continue;
            // 如果当前节点不是最上边的节点,并且当前节点上边节点是陆地的话,将其上边节点与当前节点合并
            if(i > 0 && grid[i-1][j] === '1') u.merge(idx(i, j, col), idx(i-1, j, col));
            // 如果当前节点不是最左边的节点,且当前节点左边的节点是陆地的话,将其左边的节点与当前节点合并
            if(j > 0 && grid[i][j-1] === '1') u.merge(idx(i, j ,col), idx(i, j-1, col));
        }
    }

    // 将所有的节点都通过并查集合并成一个个集合后,计算岛屿的数量
    let res = 0;

    for(let i=0;i<row;i++) {
        for(let j=0;j<col;j++) {
            // 如果当前节点是陆地并且当前节点是集合的根节点,则技术器加1
            if(grid[i][j] === '1' && u.get(idx(i, j, col)) === idx(i, j, col)) res+=1;
        }
    }
    return res;

};
// @lc code=end


990. 等式方程的可满足性
解题思路

这道题看似复杂,但仔细想想,假如说我们给定两个条件:

条件1: a=b,b=c

条件2:a!=c

我们其实很容一直到,如果a=b且b=c的话,那必然会得出结果a=c,与条件2相悖,因此这种情况是不符合提议的。

从上面的例子中,我们可以看出来,其实相等的关系也是有传递性即连通性的,我们也可以使用解决连通性问题的神兵利器——并查集来解决这个问题。

大概思路是这样:先将所有的相等关系的元素各自放到一个集合中,然后我们再遍历不等关系的两个元素,看一下他们是不是在同一个集合中,如果是,那就说明出现冲突了

代码实现
/*
 * @lc app=leetcode.cn id=990 lang=typescript
 *
 * [990] 等式方程的可满足性
 */

// @lc code=start
class UnionSet {
    // 用于存储当前节点的父节点序号
    // 虽然并查集逻辑上是树形结构,但我们实际实现时无需定义一个树形结构,因为我们想要知道的仅仅是当前节点的父级到底是谁就足够了,因此,我们使用数组来进行存储,数组的索引为当前节点的索引,值为当前节点父节点的索引
  private boss: number[];
  constructor(private n: number) {
        // 为了防止部分场景序号是从1开始的,因此初始化数组长度为n+1
      this.boss = new Array<number>(n+1);
        // 初始设置所有节点的父节点都为它本身
      for(let i=0;i<=n;i++) {
          this.boss[i] = i;
      }
  }
  // 使用路径压缩算法实现并查集的查找操作
    // 其实原理就是每查找到一个节点,我们就把节点的父级改成根节点,这样我们下次查找相同节点时,效率便可显著提升
    // 判断是否为根节点的依据:如果当前节点就等于他的根节点,即:this.boss[x]===x,就说明这个节点就是根节点
  get(x: number): number {
      return (this.boss[x] = (this.boss[x] === x ? x : this.get(this.boss[x])));
  }
      // 由于权重并查集的算法相较于路径压缩的算法带来的提升实际并不大,因为我们进行路径压缩时实际就已经极大的减少了树的层级了。因此,此处没有使用权重并查集的算法实现,直接把a树挂在b树上
  merge(a: number, b: number): void {
      this.boss[this.get(a)] = this.get(b);
  }
      // [debug] 用于调试
  output(): void {
      console.log(this.boss);
  }
}
function equationsPossible(equations: string[]): boolean {
    let res: boolean = true;
    // 根据题意,我们的变量名都是小写字母,因此,我们并查集的编号最大为26
    const u: UnionSet = new UnionSet(26);

    // 因为我们的变量是26个小写字母,为了让我们的编号从0开始,我们可以让变量字母减去a的ASCII值即可
    const offset = "a".charCodeAt(0);

    // 先将所有等式两边的变量加入到集合中
    equations.forEach(item=>{
        if(item[1] === "=") {
            const s0 = item[0].charCodeAt(0) - offset;
            const s1 = item[3].charCodeAt(0) - offset;
            u.merge(s0, s1);
        }
    });
    // 将所有不等式两边的变量取出来,如果发现在并查集中能够找到,就说明与条件相悖
    equations.forEach(item=>{
        if(item[1] === "!") {
            const s0 = item[0].charCodeAt(0) - offset;
            const s1 = item[3].charCodeAt(0) - offset;
            if(u.get(s0) === u.get(s1)) {
                res = false;
                return false;
            }
        }
    });

    return res;
};
// @lc code=end


684. 冗余连接
解题思路

根据题意,我们其实就是要找出多余的边,而这个多余的边其实并不会影响原本节点的连通性。因此,我们可以把每个节点依次的加入到并查集中,直到我们加入的某个点已经在我们的并查集的集合中时,这个点的连线就是冲突的,也就是我们要找的多余的边。

如题意中的 [[1,2], [2,3], [3,4], [1,4], [1,5]],我们依次连接如下:

1->2->3->4,当我们连接前三组数据是都是正常的,但是当我们想要去链接[1,4]时,发现两个点都已经在并查集中了,说明已经发生了冲突,因此,我们要找的冗余的边就是[1,4]

代码演示
class UnionSet {
    // 用于存储当前节点的父节点序号
    // 虽然并查集逻辑上是树形结构,但我们实际实现时无需定义一个树形结构,因为我们想要知道的仅仅是当前节点的父级到底是谁就足够了,因此,我们使用数组来进行存储,数组的索引为当前节点的索引,值为当前节点父节点的索引
  private boss: number[];
  constructor(private n: number) {
        // 为了防止部分场景序号是从1开始的,因此初始化数组长度为n+1
      this.boss = new Array<number>(n+1);
        // 初始设置所有节点的父节点都为它本身
      for(let i=0;i<=n;i++) {
          this.boss[i] = i;
      }
  }
  // 使用路径压缩算法实现并查集的查找操作
    // 其实原理就是每查找到一个节点,我们就把节点的父级改成根节点,这样我们下次查找相同节点时,效率便可显著提升
    // 判断是否为根节点的依据:如果当前节点就等于他的根节点,即:this.boss[x]===x,就说明这个节点就是根节点
  get(x: number): number {
      return (this.boss[x] = (this.boss[x] === x ? x : this.get(this.boss[x])));
  }
      // 由于权重并查集的算法相较于路径压缩的算法带来的提升实际并不大,因为我们进行路径压缩时实际就已经极大的减少了树的层级了。因此,此处没有使用权重并查集的算法实现,直接把a树挂在b树上
  merge(a: number, b: number): void {
      this.boss[this.get(a)] = this.get(b);
  }
      // [debug] 用于调试
  output(): void {
      console.log(this.boss);
  }
}
function findRedundantConnection(edges: number[][]): number[] {
    const n = edges.length;
    const u: UnionSet = new UnionSet(n);
    for(let i=0;i<n;i++) {
        const point = edges[i];
        // 如果发现两个点都在并查集中的话,就说明这个两个点的坐标的连线就是我们要找的那条冗余的边,直接返回
        if(u.get(point[0]) === u.get(point[1])) {
            return point;
        } else {
            // 将组成变的两个点加入到并查集中
            u.merge(point[0], point[1]);
        }
    }
    return [];
};
1319. 连通网络的操作次数
解题思路

这道题其实就是变相让我们求总共有多少个计算机的集群,我们想一下,题目中的示例1中,实际上有两个集群,分别是由0-1-2组成的局域网和3,那么我们实际只需要操作一次即可把这两个计算机的集群合并成一个大的集群了。同一个局域网的电脑就相当于是在同一个并查集的集合中的元素。不过有一个特殊情况我们需要额外处理一下,那就是我们的网线的数量是有限,当网线数量不够时,我们应该返回-1

代码实现
class UnionSet {
    // 用于存储当前节点的父节点序号
    // 虽然并查集逻辑上是树形结构,但我们实际实现时无需定义一个树形结构,因为我们想要知道的仅仅是当前节点的父级到底是谁就足够了,因此,我们使用数组来进行存储,数组的索引为当前节点的索引,值为当前节点父节点的索引
  private boss: number[];
  constructor(private n: number) {
        // 为了防止部分场景序号是从1开始的,因此初始化数组长度为n+1
      this.boss = new Array<number>(n+1);
        // 初始设置所有节点的父节点都为它本身
      for(let i=0;i<=n;i++) {
          this.boss[i] = i;
      }
  }
  // 使用路径压缩算法实现并查集的查找操作
    // 其实原理就是每查找到一个节点,我们就把节点的父级改成根节点,这样我们下次查找相同节点时,效率便可显著提升
    // 判断是否为根节点的依据:如果当前节点就等于他的根节点,即:this.boss[x]===x,就说明这个节点就是根节点
  get(x: number): number {
      return (this.boss[x] = (this.boss[x] === x ? x : this.get(this.boss[x])));
  }
      // 由于权重并查集的算法相较于路径压缩的算法带来的提升实际并不大,因为我们进行路径压缩时实际就已经极大的减少了树的层级了。因此,此处没有使用权重并查集的算法实现,直接把a树挂在b树上
  merge(a: number, b: number): void {
      this.boss[this.get(a)] = this.get(b);
  }
      // [debug] 用于调试
  output(): void {
      console.log(this.boss);
  }
}
function makeConnected(n: number, connections: number[][]): number {
    // 判断一下线缆数量是否足够
    if(connections.length < n-1) return -1;
    const u: UnionSet = new UnionSet(connections.length);
    // 将所有的计算机加入到并查集中
    for(let c of connections) {
        const a = c[0];
        const b = c[1];
        u.merge(a, b);
    }
    // 计算并查集的集合数,及根节点的数量
    let res = 0;
    for(let i=0;i<n;i++) {
        if(u.get(i) === i) res+=1;
    }
    return res - 1;
};
128. 最长连续序列
解题思路

遇到这样的题目,我们可能下意识的会选择先将数组排序,然后扫描一遍数组就能得出答案,但数组的排序算法是:n*log(n)的复杂度,这个不符合我们题目要就的O(n),其实我们可以换一个思路考虑这个问题,假如说我们在扫描数组的时候,当前的值为x,那么结果序列的上一个值就应该是x-1,下一个值就应该是x+1,我们可以把这三个值所在的索引使用并查集放到一个集合当中去。但是我们要怎么知道上一个值和下一个值得索引呢?我们就可以借助哈希表存储值和索引的关系来辅助我们获得上一个值和下一个值的索引。这么说可能有点抽象,我们来举个例子:

input: [1,2,0,1]

我们遍历上面的数组,当遍历到第一个元素1时,上一个元素的值应该为0,下一个元素的值应该为2,但我们要的是02的索引,现在暂时还不知道,暂缓处理,然后先将1和他的索引0用哈希表暂存起来

map: {1: 0}

接下来扫描到数组第二个元素22的上一个和下一个元素分别为100我们暂时还不知道他的索引是多少,但是1的索引我们刚刚已经存起来了,他的索引是0,所以,我们就可以把12的索引01加入到并查集的一个集合当中,最后,别忘了把2的值与索引的关系存到哈希表中

map: {1: 0, 2: 1}

接下来扫描到数组的第三个元素00上一个和下一个元素分别是-11-1我们暂时也不知道他的索引是多少,暂且不管,1的索引之前也存起来了,索引是0,所以,我们再把01的索引02存入并查集的一个集合当中。并将0的值和索引的关系保存到哈希表中

map: {1: 0, 2: 1: 0: 2}

最后扫描到数组最后一个元素1,因为我们的最长序列不考虑重复元素,1已经在我们的哈希表中有记录了,所以无需处理

到现在我们就已经扫描完了整个数组了,并且把所有的可能序列都加入到了并查集的一个个集合当中了,那么,我们要找最长序列的话,其实就去并查集中找一下元素最多的那个集合的长度即可。

代码实现
class UnionSet {
    // 用于存储当前节点的父节点序号
    // 虽然并查集逻辑上是树形结构,但我们实际实现时无需定义一个树形结构,因为我们想要知道的仅仅是当前节点的父级到底是谁就足够了,因此,我们使用数组来进行存储,数组的索引为当前节点的索引,值为当前节点父节点的索引
  private boss: number[];
  // 纪录每个集合元素数量
  private size: number[];
  constructor(private n: number) {
        // 为了防止部分场景序号是从1开始的,因此初始化数组长度为n+1
      this.boss = new Array<number>(n+1);
      this.size = new Array<number>(n+1);
        // 初始设置所有节点的父节点都为它本身
      for(let i=0;i<=n;i++) {
          this.boss[i] = i;
          this.size[i] = 1;
      }
  }
  // 使用路径压缩算法实现并查集的查找操作
    // 其实原理就是每查找到一个节点,我们就把节点的父级改成根节点,这样我们下次查找相同节点时,效率便可显著提升
    // 判断是否为根节点的依据:如果当前节点就等于他的根节点,即:this.boss[x]===x,就说明这个节点就是根节点
  get(x: number): number {
      return (this.boss[x] = (this.boss[x] === x ? x : this.get(this.boss[x])));
  }
      // 由于权重并查集的算法相较于路径压缩的算法带来的提升实际并不大,因为我们进行路径压缩时实际就已经极大的减少了树的层级了。因此,此处没有使用权重并查集的算法实现,直接把a树挂在b树上
  merge(a: number, b: number): void {
      if(this.get(a) === this.get(b)) return;
      this.size[this.get(b)] += this.size[this.get(a)];
      this.boss[this.get(a)] = this.get(b);
      
  }

  // 计算并查集中元素最多的集合的元素数量
  maxSize(): number {
      return Math.max(...this.size.filter((item, idx) => this.get(idx) === idx));
  }

      // [debug] 用于调试
  output(): void {
      console.log(this.boss);
  }
}

function longestConsecutive(nums: number[]): number {
    // 如果原始数组为空数组,则直接返回0
    if(nums.length===0) return 0;
    // 使用一个哈希表存出值与索引的关系
    const map: Record<number, number> = {};
    // 初始化一个并查集
    const u: UnionSet = new UnionSet(nums.length);
    for(let i=0;i<nums.length;i++) {
        const x = nums[i];
        // 如果发现当前值已经在哈希表中存在时,就无须进行后续操作,直接进入下一次循环
        if(map[x]!==undefined) continue;
        // 查找当前值得上一位是否在哈希表中存在,如果存在,则让当前索引与上一位的索引合并到一个集合中
        if(map[x-1]!==undefined) {
            u.merge(i, map[x-1]);
        }
        // 查找当前值得下一位是否在哈希表中存在,如果存在,则让当前索引与下一位的索引合并到一个集合中
        if(map[x+1]!==undefined) {
            u.merge(i, map[x+1]);
        }
        // 最后存储值和索引的关系到哈希表中
        map[x] = i;
    }
    // 最后,最长序列的长度就是我们并查集中元素最多的集合的元素数量
    return u.maxSize();
};
947. 移除最多的同行或同列石头
解题思路

这道题我们可以理解为如果石头处于同一行或同一列,那么这些石头就可以连通成为一个集合,而我们要找的可以移除的石头数量,其实就是石头的总数量减去我们的集合数量(因为集合数量就等于最后剩下来的石头数量),也就是说,实际上我们每个集合只要保留一块石头就够了。接下来,我们就要解决要如何将石头加入到并查集中了,我们可以分别处理x坐标相同和y坐标相同的石头,因为无论是x坐标相同还是y坐标相同,都应该是统一个集合的。

代码实现
class UnionSet {
    // 用于存储当前节点的父节点序号
    // 虽然并查集逻辑上是树形结构,但我们实际实现时无需定义一个树形结构,因为我们想要知道的仅仅是当前节点的父级到底是谁就足够了,因此,我们使用数组来进行存储,数组的索引为当前节点的索引,值为当前节点父节点的索引
  private boss: number[];
  constructor(private n: number) {
        // 为了防止部分场景序号是从1开始的,因此初始化数组长度为n+1
      this.boss = new Array<number>(n+1);
        // 初始设置所有节点的父节点都为它本身
      for(let i=0;i<=n;i++) {
          this.boss[i] = i;
      }
  }
  // 使用路径压缩算法实现并查集的查找操作
    // 其实原理就是每查找到一个节点,我们就把节点的父级改成根节点,这样我们下次查找相同节点时,效率便可显著提升
    // 判断是否为根节点的依据:如果当前节点就等于他的根节点,即:this.boss[x]===x,就说明这个节点就是根节点
  get(x: number): number {
      return (this.boss[x] = (this.boss[x] === x ? x : this.get(this.boss[x])));
  }
      // 由于权重并查集的算法相较于路径压缩的算法带来的提升实际并不大,因为我们进行路径压缩时实际就已经极大的减少了树的层级了。因此,此处没有使用权重并查集的算法实现,直接把a树挂在b树上
  merge(a: number, b: number): void {
      this.boss[this.get(a)] = this.get(b);
  }
      // [debug] 用于调试
  output(): void {
      console.log(this.boss);
  }
}

function removeStones(stones: number[][]): number {
    const n = stones.length;
    const u: UnionSet = new UnionSet(n);
    // 用于存储相同行的石头编号
    const mapX: Record<number, number> = {};
    // 用于存储相同列的石头的编号
    const mapY: Record<number, number> = {};

    for(let i=0;i<n;i++) {
        const p = stones[i];
        const x = p[0];
        const y = p[1];
        // 如果存在向同行的时候,则将当前石头编号和其对应行的第一块石头的编号合并到同一个集合中
        if(mapX[x]!==undefined) {
            u.merge(i, mapX[x]);
        }
        // 如果存在向同列的时候,则将当前石头编号和其对应列的第一块石头的编号合并到同一个集合中
        if(mapY[y]!==undefined) {
            u.merge(i, mapY[y]);
        }
        // 将行列编号暂存
        mapX[x] = i;
        mapY[y] = i;
    }
    // 计算并查集的总集合数
    let count = 0;
    for(let i=0;i<n;i++) {
        if(u.get(i) === i) count+=1;
    }
    // 可移除的石头数量就是石头总数减去集合总数
    return n - count;
};
1202. 交换字符串中的元素
解题思路

根据题意,输入的pairs数组其实本身就可以看做是一个连通关系,我们就可以建立一个并查集维护这个连通关系,如果想要字典序最小,那么我们可以让同一个集合中的元素按照ASCII升序排序,这样,每一个集合都是最小的字典序了。至于排序的实现,我们可以用我们之前学过的小顶堆来实现,不清楚小顶堆概念的同学可以看一下之前的文章。

代码实现
class UnionSet {
    // 用于存储当前节点的父节点序号
    // 虽然并查集逻辑上是树形结构,但我们实际实现时无需定义一个树形结构,因为我们想要知道的仅仅是当前节点的父级到底是谁就足够了,因此,我们使用数组来进行存储,数组的索引为当前节点的索引,值为当前节点父节点的索引
  private boss: number[];
  constructor(private n: number) {
        // 为了防止部分场景序号是从1开始的,因此初始化数组长度为n+1
      this.boss = new Array<number>(n+1);
        // 初始设置所有节点的父节点都为它本身
      for(let i=0;i<=n;i++) {
          this.boss[i] = i;
      }
  }
  // 使用路径压缩算法实现并查集的查找操作
    // 其实原理就是每查找到一个节点,我们就把节点的父级改成根节点,这样我们下次查找相同节点时,效率便可显著提升
    // 判断是否为根节点的依据:如果当前节点就等于他的根节点,即:this.boss[x]===x,就说明这个节点就是根节点
  get(x: number): number {
      return (this.boss[x] = (this.boss[x] === x ? x : this.get(this.boss[x])));
  }
      // 由于权重并查集的算法相较于路径压缩的算法带来的提升实际并不大,因为我们进行路径压缩时实际就已经极大的减少了树的层级了。因此,此处没有使用权重并查集的算法实现,直接把a树挂在b树上
  merge(a: number, b: number): void {
      this.boss[this.get(a)] = this.get(b);
  }
      // [debug] 用于调试
  output(): void {
      console.log(this.boss);
  }
}
export type HeapDataStruct<T> = {
    idx?: number,
    data: T
};
export type CompareFn<T> = (x: HeapDataStruct<T>, y: HeapDataStruct<T>) => boolean;
class Heap<T> {
    private arr: HeapDataStruct<T>[] = [];
    private count: number = 0;
    constructor(private cmpFn: CompareFn<T>) {

    }

    private _compare(curIdx: number, pIdx: number): boolean {
        return this.cmpFn(this.arr[curIdx], this.arr[pIdx])
    }

    private _shift_up(idx: number): void {
        // 然后对堆进行向上调整
        // 我们需要通过子节点坐标得到父节点的坐标,因为我们这边的下标是从0开始的,所以左子树根节点坐标为:2 * i + 1,右子树根节点坐标为:2 * i + 2。那么我们父节点的编号:parseInt((子节点编号 - 1) / 2),如知道左节点编号为:2 * i + 1,那么他的父节点坐标就是: parseInt((2 * i + 1 - 1) / 2) = parseInt(i);如果知道有节点的编号:2 * i + 2,那么他的父节点的坐标为:parseInt((2 * i + 2 - 1) / 2) = parseInt((2*i+1)/2) = i
        // 上面我们讲过,我们需要如果在一个大顶堆中,如果我们父节点的值小于子节点的值的话,就要交换这两个值
        // while(idx && this.arr[parseInt(String((idx - 1) / 2))].data < this.arr[idx].data) {
        while(idx && this._compare(parseInt(String((idx - 1) / 2)), idx)) {
          // 父节点编号
          const pIdx = parseInt(String((idx - 1) / 2));
          // 交换父节点和当前节点的值
          [ this.arr[pIdx], this.arr[idx] ] = [ this.arr[idx], this.arr[pIdx] ];
          // 然后再让idx变成父节点的编号,这样就能依次向上调整了
          idx = pIdx;
        }
      }

      private _shift_down(idx: number): void {
        // 交换之后,根节点再依次向下调整
        // 最大的子节点的下标
        let n = this.count - 1;
        // 当idx的子节点的下标比最大的子节点的下标小,就说明还有子节点,就要往下继续调整
        while(idx * 2 + 1 <= n) {
          // max代表在根节点、左子树、右子树中最大值的下标
          let max = idx;
          // 如果左子树的根节点的值比当前值大,就把max更新为左子树根节点的下标
        //   if(this.arr[max].data < this.arr[2*idx+1].data) max = 2*idx+1;
          if(this._compare(max, 2*idx+1)) max = 2*idx+1;
          // 如果右子树根节点的值比当前节点大,就把max更新为右子树根节点的下标
          // 需要注意的是,我们当前的节点,可能存在左子树,但不一定存在右子树,所以需要多加一个2*idx+2<=n的条件
          if(2*idx+2<=n && this._compare(max, 2*idx+2)) max = 2*idx+2;
        //   if(2*idx+2<=n && this.arr[max].data < this.arr[2*idx+2].data) max = 2*idx+2;
          // 如果我的最大值的下标就是当前的这个值,那就不需要向下调整了,直接结束循环
          if(max === idx) break;
          // 然后交换这个最大值的下标和根节点
          [ this.arr[idx], this.arr[max] ] = [ this.arr[max], this.arr[idx] ];
          // 然后把idx改为原最大值的下标,继续向下调整
    
          idx = max;
        }
      }

    push(item: HeapDataStruct<T>): void {
        this.arr[this.count++] = item;
        this._shift_up(this.count-1);
    }

    pop(): HeapDataStruct<T>|null {
      if(this.size()===0) return null;
      const tmp = this.arr[0];
      // 根据上面的讲解,我们首先需要让队尾元素与根节点交换
      [ this.arr[0], this.arr[this.count-1] ] = [ this.arr[this.count-1], this.arr[0] ];
      // 交换之后记得count要减1
      this.count--;
      // 进行向下调整
      this._shift_down(0);
      return tmp;
    }

    size(): number {
        return this.count;
    }

    top(): HeapDataStruct<T> {
        return this.arr[0];
    }

    output(): void {
        console.log(JSON.stringify(this.arr));
    }
}
function smallestStringWithSwaps(s: string, pairs: number[][]): string {
    const u: UnionSet = new UnionSet(s.length);
    const heaps: Heap<string>[] = [];

    // 建立字符的连通关系
    for(const p of pairs) {
        u.merge(p[0], p[1]);
    }

    // 将每一个字符都加入到各自集合的小顶堆中
    for(let i=0;i<s.length;i++) {
        const c = s[i];
        // 如果不存在小顶堆,则新建小顶堆,并将小顶堆存放到第i为字符集合根元素的索引为索引的数组项中,代表该集合的小顶堆
        (heaps[u.get(i)]||(heaps[u.get(i)] = new Heap<string>((a, b) => a.idx - b.idx > 0))).push({
            idx: c.charCodeAt(0),
            data: c
        });
    }

    // 拼接结果字符串,每次弹出每个字符相应小顶堆的对顶元素并拼接到结果字符串
    let res = "";
    for(let i=0;i<s.length;i++) {
        res += heaps[u.get(i)].pop().data;
    }
    return res;
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星河阅卷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值