【从蛋壳到满天飞】JS 数据结构解析和算法实现-并查集(一)

思惟导图

前言

【从蛋壳到满天飞】JS 数据结构解析和算法实现,所有文章大概的内容以下: Arrays(数组)、Stacks(栈)、Queues(队列)、LinkedList(链表)、Recursion(递归思想)、BinarySearchTree(二分搜索树)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(优先队列)、SegmentTree(线段树)、Trie(字典树)、UnionFind(并查集)、AVLTree(AVL 平衡树)、RedBlackTree(红黑平衡树)、HashTable(哈希表)html

源代码有三个:ES6(单个单个的 class 类型的 js 文件) | JS + HTML(一个 js 配合一个 html)| JAVA (一个一个的工程)git

所有源代码已上传 github,点击我吧,光看文章可以掌握两成,动手敲代码、动脑思考、画图才能够掌握八成。github

本文章适合 对数据结构想了解而且感兴趣的人群,文章风格一如既往如此,就以为手机上看起来比较方便,这样显得比较有条理,整理这些笔记加源码,时间跨度也算将近半年时间了,但愿对想学习数据结构的人或者正在学习数据结构的人群有帮助。算法

并查集 Union Find

  1. 并查集是一种很不同的树形结构
    1. 以前的树结构都是由父亲指向孩子,
    2. 可是并查集是由孩子指向父亲而造成的这样的一种树结构,
    3. 这样一种奇怪的树结构能够很是高效的来解决某一类问题,
    4. 这类问题就是链接问题(Connectivity Problem),
    5. 并查集是一种能够高效的回答链接问题的这样的一种数据结构。
  2. 链接问题
    1. 给出一个图中任意的两点,
    2. 这两点之间是否能够经过一个路径链接起来,
    3. 简单的使用肉眼观察距离很近的两点,是能够观察出来的,
    4. 若是两点的距离很远,两点之间隔着还有无数个点,
    5. 那么你就很难用肉眼观察出来它们是不是相连的,
    6. 此时就须要借助必定的数据结构,
    7. 而并查集就是回答这种链接问题一个很是好一种数据结构。
  3. 并查集能够很是快的判断网络中节点的链接状态
    1. 这里的网络其实是一个抽象的概念,
    2. 不只仅是在计算机领域所使用的互联网这样的一个网络,
    3. 最典型的一个例子,如社交网络、微博、微信、facebook,
    4. 他们之间其实就是由一个一个的人做为节点造成的一个网络,
    5. 在这种时候就能够把每两个用户之间是否是好友关系,
    6. 这样的一个概念给抽象成两个节点之间的边,
    7. 若是能够这样的创建一个网络的话,相应的就会产生链接问题,
    8. 好比两个用户 A 和 B,他们原本多是互不认识的,
    9. 那么经过这个网络是否有可能经过认识的人他认识的人,
    10. 这样一点点的扩散,最终接触到那个你原本彻底不认识的人,
    11. 这样的一个问题其实就是在社交网络中相应的链接问题。
  4. 网络这样的一种结构不只仅是用在社交网络
    1. 不少信息网络,好比说亚马逊的商品、豆瓣儿的图书、
    2. 或者音乐网站的一些音乐专辑,这些内容均可以造成节点,
    3. 节点之间均可以以某种形式来定义边,从而造成一个巨大的网络,
    4. 能够在这样的网络中作很是多的事情,
    5. 好比交通系统、公交车、火车、飞机等航班与航线之间他们全都是网络,
    6. 更不用提计算机的网络,每个路由器都是一个节点,
    7. 其实网络自己是一个应用很是普遍的概念,
    8. 在实际中处理的不少问题,把它抽象出来可能都是一个网络上的问题,
  5. 在回答网络中的节点的链接状态这样的一个问题的时候,
    1. 并查集就是一个很是强力的性能很是高效的数据结构,
    2. 并查集除了能够高效的回答网络中节点间的链接状态的问题以外,
    3. 仍是数学中集合这种类的一个很好的实现,
    4. 若是你使用的集合主要的操做是在求两个集合的并集的时候,
    5. 并查集中其实就是集合中的这样的概念,
    6. 相应的查就是一个查询操做。
  6. 对于并查集来讲他们很是高效的来回答在网络中两个节点是否链接的问题
    1. 在一个网络也是能够两个节点他们之间的路径是怎样的,
    2. 既然能够求出两个节点之间的路径,其实就回答了链接的问题,
    3. 两个节点之间若是存在一个路径,那么就必定是链接的,
    4. 若是这个路径根本就不存在,那么它确定是不链接的,
    5. 这样的一个思路确定是正确的,
    6. 若是想要回答两个节点之间的链接问题,
    7. 这个答案实际上是比回答两个节点之间的路径问题回答的内容要少的,
    8. 由于只须要返回 true 或者 false 就行了,
    9. 可是若是要问 A 和 B 之间的路径是什么的话,
    10. 那么相应的就要获得一个从 A 节点出发一步一步达到节点 B,
    11. 这样一个具体的路径,换句话说其实回答路径问题的方式
    12. 来回答链接问题,那么真正回答的内容是更加的多了,
    13. 这样会致使结果消耗了一些额外的性能求出了当前不关心的内容,
    14. 那个内容就是 A 和 B 之间的具体路径是什么。
  7. 当你深刻的学习数据结构和算法
    1. 慢慢的就会发现不少问题都会存在这样的状况,
    2. 你彻底可使用一个复杂度更高的算法来把这个问题求解出来,
    3. 可是这个算法之因此复杂度比较高,
    4. 就是由于其实它求出了你问的那个问题并不关心的内容,
    5. 例如本身实现的堆,彻底可使用顺序表示这样的结构,
    6. 或者直接使用一个线性结构数组或链表,
    7. 而后保持这个线性结构中全部元素都是有序的,
    8. 堆这种结构每次都要取出最大或最小的那个元素,
    9. 使用这种顺序表示是很是容易实现的,
    10. 但关键在于使用顺序表示不只仅能够很是高效的取出
    11. 那个最大的元素或者最小的元素,还能够很是高效的取出
    12. 你存储的第二大的元素或者第二小的元素,
    13. 而这些内容都是在应用堆这种数据结构的时候其实不关心的,
    14. 在系统调度的时候只关系那个优先级最大的任务,
    15. 在医院医生决定作手术的时候只关心那个当前优先级最高的患者,
    16. 为他来准备手术,在涉及一个游戏 AI 的时候,
    17. 当前控制的那个小机器人只能选择一个对你威胁最大的敌人来攻击,
    18. 因此在这种状况下使用顺序表示,它其实维护了不少这些应用中并
    19. 不须要的信息,为了维护这些信息,它就须要有额外性能消耗,
    20. 要维持一个彻底的顺序表示,在插入元素的时候时间复杂度是O(n)
    21. 这个级别的,之因此会产生这样的状况,
    22. 由于它不只仅是维护了当前数据中最大的或者最小的那个元素,
    23. 而堆这种数据结构除了你关心的那个最大的元素和最小的元素以外,
    24. 无论其它元素之间的顺序,这才使得堆这种数据结构相比顺序表来讲,
    25. 总体大大提升了它的性能。
  8. 链接问题和路径问题也是同样的
    1. 虽然可使用求解路径的思路来看 A 和 B 这两个点是否链接,
    2. 可是因为它回答了额外的问题,A 和 B 之间具体怎么链接都回答出来了,
    3. 在不少时候并不关心 A 和 B 之间怎么链接,只要看他是否链接,
    4. 此时并查集就是一种更好的选择,对于这一点,
    5. 不少算法或者数据结构它们所解决的问题之间的差异是很是微妙的,
    6. 须要不断的积累不断的实践,慢慢的了解每种不一样的算法或者不一样的数据结构,
    7. 它们所解决的那个问题以及具体的不一样点在哪里,
    8. 时间久了就能够慢慢的很是快速的反应出对于某一些具体问题
    9. 最好的应该使用哪一种算法或者哪一种数据结构来进行解决。
  9. 具体来说对于并查集这种数据结构来讲
    1. 存储一组数据,它主要能够支持两个动做,
    2. union(p, q),也就是并的操做,传入两个参数 p 和 q,
    3. 而后在并查集内部将这两个数据以及他们所在的集合给合并起来,
    4. 另一个动做就是isConnected(p, q)
    5. 也就是查询对于给定的两个数据,他们是否属于同一个集合,
    6. 并查集主要支持这样的两种操做。
  10. 须要设计这样的一种并查集接口
    1. 也就是说并查集也能够有不一样的底层实现,
    2. 经过实现不一样的并查集,
    3. 能够一点点的优化本身实现的并查集,
    4. 随着你不断的优化,
    5. 本身编写的这个并查集在具体的解决链接问题的时候,
    6. 效率会愈来愈高。

并查集 简单实现

  1. MyUnionFind
    1. unionElements(p, q):将这两个数据以及他们所在的集合进行合并。
    2. isConnected(p, q):查询两个数据是否在同一个集合中。
    3. getSize():当前并查集一共考虑多少个元素
  2. isConnected 方法中传入的 p 和 q 都是 int 型,
    1. 对于具体元素是谁,在并查集的内部并不关心,
    2. 在使用并查集的时候能够将元素和一个数组相对应的数组索引作一个映射
    3. 至关于真正关心的是一个 id 为 p 和 id 为 q 这样的两个元素它们是否相连,
    4. 对于 id 为 p 这样的元素它具体对应的是什么样的一个元素并不关心。
  3. unionElements 方法中传入的 p 和 q 都是 int 型。
  4. 向线段树同样,并不考虑添加一个元素或者删除一个元素
    1. 考虑的是对于当下固定的元素来讲,
    2. 进行并或者查这样的两个操做。

代码示例

  1. MyUnionFind编程

    // 自定义并查集 UnionFind
    class MyUnionFind {
       // 功能:将元素q和元素p这两个数据以及他们所在的集合进行合并
       unionElements(q, p) {}
    
       // 功能:查询元素q和元素p这两个数据是否在同一个集合中
       isConnected(q, p) {}
    
       // 功能:当前并查集一共考虑多少个元素
       getSize() {}
    }
    复制代码

并查集 简单实现 Quick Find

  1. 对于并查集主要实现两个操做api

    1. union 操做将两个元素合并在一块儿变成在一个集合中的元素,
    2. isConnected 操做查看两个元素是不是相连的
  2. 并查集的基本数据表示数组

    1. 能够直接给每一个数据作一个编号,
    2. 0-9 就表示 10 个不一样的数据,
    3. 这是一种抽象的表示,
    4. 具体这十个编号多是十我的或者是十部车或者是十本书,
    5. 这是由你的业务逻辑所决定的,
    6. 可是在并查集的内部只存 0-9 这是个编号,
    7. 它表示十个具体的元素
    8. 对于每个元素它存储的是对应的集合的 ID。
    9. 例以下图并查集一中编号 0-4 这五个数据它们所对应的 ID 为 0,
    10. 编号为 5-9 这五个数据它们所对应的 ID 为 1,
    11. 不一样的 ID 值就是不一样的集合所对应的那个编号,
    12. 在并查集中就能够表示为 将这个十个数据分红了两个集合,
    13. 其中 0-4 这五个元素在一个集合中,5-9 这个五个元素在另外一个集合中。
    14. 若是是下图并查集二中这样子,
    15. 其中 0、二、四、六、8 这五个元素在一个集合中,
    16. 而 一、三、五、七、9 这五个元素在一个集合中,
    17. 在具体的编程中会把这样的一个数组称之为 id,
    18. 经过这样的一个数组就能够很是容易的来回答所谓的链接问题,
    19. 在并查集图二中,0 和 2 就是相链接的,
    20. 或者说 0 和 2 是同属于一个集合的,由于他们所对应的 id 的值都是 0,
    21. 1 和 3 也属于同一个同一个集合,由于他们所对应的 id 值都为 1,
    22. 相应的能够想象 1 和 2 都属于不一样的集合,由于他们对应的 id 值是不一样的。
    // 并查集 一
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // id 0 0 0 0 0 1 1 1 1 1
    
    // 并查集 二
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // id 0 1 0 1 0 1 0 1 0 1
    复制代码
  3. 使用 id 这样的一个数组来存储你的数据微信

    1. 是能够很容易的回答 isConnected 的这个问题的,
    2. 只须要直接来看 p 和 q 这两个值所对应的 id 值是否同样就行了,
    3. 将查询 p 或者 q 每一个元素背后所对应的那个集合的 id 是谁也
    4. 抽象成一个函数,这个函数就叫作 find,
    5. 只须要看find(p)是否等于find(q)就行了。
  4. 当你使用 find 函数进行操做的时候只须要O(1)的时间复杂度网络

    1. 直接取出 id 这个数组所对应的这个数据的 Index 相应值便可,
    2. 因此对于这种存储方式在并查集上进行 find 操做时是很是快速的,
    3. 这种并查集的方式一般称为 QuickFind,
    4. 也就是对于 find 这种操做运算速度是很是快的。
      // 并查集
      // 0 1 2 3 4 5 6 7 8 9
      // -------------------------------------
      // id 0 1 0 1 0 1 0 1 0 1
      复制代码
  5. QuickFind 方式的并查集中实现 union数据结构

    1. 若是想要合并 1 和 4 这两个索引所对应的元素,也就是union(1, 4)
    2. 1 所对应的集合的 id 是 1,4 所对应的集合的 id 是 0,
    3. 在这种状况下将 1 和 4 这两个元素合并之后,
    4. 其实 1 所属的那个集合和 4 所属的那个集合每个元素至关于也链接了起来,
    5. 原本 一、三、五、七、9 它们是链接在一块儿的,0、二、四、六、8 它们是链接在一块儿的,
    6. 而 1 和 4 并无链接起来,可是一旦你将 1 和 4 链接起来以后,
    7. 本来和 1 链接的其它元素以及本来和 4 链接的其它元素,
    8. 好比 5 和 2,它们其实也就都链接起来了,通过这样的操做以后,
    9. 全部的奇数所表示的元素和全部的偶数所表示的元素它们所对应的集合
    10. 的 id 值应该都会变成同样的,应该都是 0 或者都是 1,
    11. 具体取 0 仍是取 1 都是无所谓的,只要他们的值是同样的就行了,
    12. 就会变成下图union(1, 4)后的并查集,
    13. 具体实现是对整个 id 数组进行一遍循环,
    14. 在循环的过程当中将全部的 id 值等于 0 所对应的那个元素的 id 值都改写成 1,
    15. 正是由于如此 QuickFind 方式的并查集实现的 union 的时间复杂度是O(n)
    16. 因此这个 union 操做须要改进,也就是建立一棵树,这棵树很是的奇怪,
    17. 是由孩子指向父亲的,而当前实现的这个并查集只是用数组模拟了一下而已。
    // 并查集
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // id 0 1 0 1 0 1 0 1 0 1
    
    // 并查集 union(1, 4)以后的并查集
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // id 1 1 1 1 1 1 1 1 1 1
    复制代码

代码示例

  1. MyUnionFindOne

    // 自定义并查集 UnionFind 第一个版本 QuickFind版
    // isConnected 操做很快
    class MyUnionFindOne {
       constructor(size) {
          // 存储数据所对应的集合的编号
          this.ids = new Array(size);
    
          // 模拟存入数据
          const len = this.ids.length;
          for (var i = 0; i < len; i++) this.ids[i] = i;
       }
    
       // 功能:将元素q和元素p这两个数据以及他们所在的集合进行合并
       // 时间复杂度:O(n)
       unionElements(q, p) {
          const qId = this.find(q);
          const pId = this.find(p);
    
          if (qId === pId) return;
    
          for (var i = 0; i < this.ids.length; i++)
             if (pId === this.ids[i]) this.ids[i] = qId;
       }
    
       // 功能:查询元素q和元素p这两个数据是否在同一个集合中
       // 时间复杂度:O(1)
       isConnected(q, p) {
          return this.ids[q] === this.ids[p];
       }
    
       // 查找元素所对应的集合编号
       find(index) {
          if (index < 0 || index >= this.ids.length)
             throw new Error('index is out of bound.');
          return this.ids[index];
       }
    
       // 功能:当前并查集一共考虑多少个元素
       getSize() {
          return this.ids.length;
       }
    }
    复制代码

并查集 简单实现 Quick Union

  1. QuickFind 的方式实现的并查集查找速度很是快

    1. 可是一般在标准状况下都是使用 QuickUnion 的方式实现并查集。
  2. QuickUnion 的方式实现并查集思路

    1. 将每个元素,看做是一个节点,而节点之间相链接造成了一个树结构,
    2. 这棵树和以前实现的全部的树都不同,
    3. 在并查集上实现的树结构是孩子指向父亲,
    4. 例如节点 3 指向节点 2,那么节点 2 就是这棵树的根节点,
    5. 虽然节点 2 是一个根节点,可是它也有一个指针,这个指针指向的是本身,
    6. 在这种状况下若是节点 1 要和节点 3 进行一个合并,
    7. 这个合并操做就是就是让节点 1 的指针指向节点 3 指向的这棵树的根节点,
    8. 也就是让节点 1 去指向节点 2。
    9. 若是又有一棵树 节点 7 和节点 6 都指向节点 5,节点 5 是这棵树的根节点,
    10. 可是若是要节点 7 要和节点 2 作一下合并,
    11. 其实就是就是让节点 7 所在的这棵树的根节点也就是节点 5 去指向节点 2,
    12. 或者你是想让节点 7 和节点 3 进行一下合并,
    13. 那么的获得的结果依然是这样的,由于实际的操做是让
    14. 节点 7 所在的这棵树的根节点去指向节点 3 所在的这棵树的根节点,
    15. 依然是节点 5 去指向节点 2,因此依然获得相同的结果,
    16. 这就是实际实现并查集相应的思路。
    // (5) (2)
    // / \ | \
    // / \ | \
    // (6) (7) (3) (1)
    复制代码
  3. QuickUnion 的方式实现并查集很是的简单

    1. 由于每个节点自己只有一个指针,只会指向另一个元素,
    2. 而且这个指针的存储依然可使用数组的方式来存储,
    3. 这个数组就叫作 parent,
    4. parent[i]就表示第 i 个元素所在的那个节点它指向了哪一个元素,
    5. 虽说是指针,可是实际存储的时候依然使用一个 int 型的数组就够了,
    6. 这样一来在初始化的时候parent[i] = i
    7. 也就是初始化的时候每个节点都没有和其它的节点进行合并,
    8. 因此在初始化的时候每个节点都指向了本身,
    9. 在这种状况下至关于 以 10 个元素为例,并查集总体就是下图这样子,
    10. 每个节点都是一个根节点,它们都指向本身。
    11. 严格的来讲这个并查集不是一棵树结构,而是一个森林,
    12. 所谓的森林就是说里面有不少的树,在初始的状况下,
    13. 这个森林中就有 10 棵树,每棵树都只有一个节点,
    14. 若是进行union(4, 3)操做,
    15. 那么直接让节点 4 的的指针去指向节点 3 就行了,
    16. 这样的一个操做在数组中表示出来就是parent[4] = 3
    17. 那么节点 4 它指向了节点 3,若是在进行union(3, 8)操做,
    18. 那么就让节点 3 的指针指向的那个元素指向节点 8,
    19. 那么在数组中parent[3] = 8,再进行union(6, 5)操做,
    20. 那么就让节点 6 的指针指向的那个元素指向节点 5,
    21. 也就是parent[6] = 5,再进行union(9, 4)操做,
    22. 那么就让节点 9 的指针指向指向节点 4 这棵树的根节点,
    23. 那么在这里就有一个查询操做了,
    24. 那么就要看一下 4 这个节点所在的根节点是谁,
    25. 这个查询过程就是 节点 4 指向了节点 3,节点 3 又指向了节点 8,
    26. 而节点 8 本身指向了节点 8 也就是指向了本身,说明 8 是一个根节点,
    27. 那么下面要作的事情就是让 9 这个节点指向节点 8 就行了
    28. 也就是parent[9] = 8,之因此不让节点 9 指向节点 4,
    29. 由于那样的话就会造成一个链表,那么树总体的优点就体现不出来,
    30. 当你的节点 9 指向节点 8,下次你查询节点 9 的根节点只须要进行一步查询,
    31. 因此才让parent[9] = 8,再进行union(2, 1)操做,
    32. 直接让节点 2 指向节点 1 就行了,parent[2] = 1
    33. 再进行union(5, 0)操做,直接让节点 5 指向节点 0 就行了,
    34. parent[5] = 0,再进行union(7, 2)操做,
    35. 因为节点 2 指向节点 1,那么节点 7 就要指向节点 1,
    36. parent[7] = 1
    37. 接下来进行一个稍微复杂一点的操做,进行union(6, 2)操做,
    38. 因为节点 6 指向节点 5,而节点 5 指向节点 0,2 指向节点 1,
    39. 那么就是让节点 0 指向节点 1 了,因此parent[0] = 1
    40. 这样的一种实现就是并查集一般真正的实现方式。
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // parent 0 1 2 3 4 5 6 7 8 9
    //
    // Quick Union
    // (0) (1) (2) (3) (4) (5) (6) (7) (8) (9)
    //
    // 一通以下操做
    // union(4, 3); // 4->3
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 2 3 3 5 6 7 8 9
    //
    // union(3, 8); // 3->8
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 2 8 3 5 6 7 8 9
    //
    // union(6, 5); // 6->5
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 2 8 3 5 5 7 8 9
    //
    // union(9, 4); // 4->3 3->8 因此 9->8
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 2 8 3 5 5 7 8 8
    //
    // union(2, 1); // 2->1
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 1 8 3 5 5 7 8 8
    //
    // union(5, 0); // 5->0
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 1 8 3 0 5 7 8 8
    //
    // union(7, 2); // 2->1 因此 7->1
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 0 1 1 8 3 0 5 1 8 8
    //
    // union(6, 2); // 6->5 5->0,2->1 因此0->1
    // 0 1 2 3 4 5 6 7 8 9
    // -------------------------------------
    // 1 1 1 8 3 0 5 1 8 8
    复制代码
  4. QuickUnion 的方式实现并查集中的 union 操做的时间复杂度是O(h)

    1. 这个 h 是当前 union 的这两个元素它所在的树相应的深度大小,
    2. 这个深度的大小在一般的状况下都比元素的个数 n 要小,
    3. 因此 union 的这个过程相对以前要快一些,
    4. 不过相应的代价就是 查询的过程相应的时间复杂度依然是树的深度大小,
    5. 因此就稍微牺牲了一些查询时相应的性能,
    6. 不过因为在一般状况下这棵树的高度是远远小于数据总量 n 的,
    7. 因此要让合并和查询这两个操做都是树的高度这个时间复杂度,
    8. 相应的在大多数运用中这个性能是能够接受的,
    9. 固然目前实现的并查集仍是有很大的优化空间的。
  5. 这个版本的并查集虽然是使用数组来进行存储的

    1. 可是它其实是一种很是奇怪的树,这种树是由孩子指向父亲的。

代码示例

  1. MyUnionFindTwo

    // 自定义并查集 UnionFind 第二个版本 QuickUnion版
    // Union 操做变快了
    // 还能够更快的
    class MyUnionFindTwo {
       constructor(size) {
          // 存储当前节点所指向的父节点
          this.forest = new Array(size);
    
          // 在初始的时候每个节点都指向它本身
          // 也就是每个节点都是独立的一棵树
          const len = this.forest.length;
          for (var i = 0; i < len; i++) this.forest[i] = i;
       }
    
       // 功能:将元素q和元素p这两个数据以及他们所在的集合进行合并
       // 时间复杂度:O(h) h 为树的高度
       unionElements(treePrimary, treeSecondary) {
          const primaryRoot = this.find(treePrimary);
          const secondarRoot = this.find(treeSecondary);
    
          if (primaryRoot === secondarRoot) return;
    
          // 不管哪棵树往那棵树上进行合并 都同样,他们都是树
          // 这里是主树节点上往次树节点进行合并
          this.forest[primaryRoot] = this.forest[secondarRoot];
       }
    
       // 功能:查询元素q和元素p这两个数据是否在同一个集合中
       // 时间复杂度:O(h) h 为树的高度
       isConnected(treeQ, treeP) {
          return this.find(treeQ) === this.find(treeP);
       }
    
       // 查找元素所对应的集合编号
       find(id) {
          if (id < 0 || id >= this.ids.length)
             throw new Error('index is out of bound.');
    
          // 不断的去查查找当前节点的根节点
          // 根节点的索引是指向本身,若是根节点为 1 那么对应的索引也为 1。
          while (id !== this.forest[id]) id = this.forest[id];
    
          return id;
       }
    
       // 功能:当前并查集一共考虑多少个元素
       getSize() {
          return this.ids.length;
       }
    }
    复制代码

并查集 Quick Union 基于 Size 的优化

  1. 两版并查集的比较
    1. 第二版的 QuickUnion 方式的并查集和
    2. 初版 QuickFind 方式的并查集在思路上有很是大的不一样,
    3. 初版的并查集实际上就是使用数组来模拟每一个数据所属的集合是谁,
    4. 第二版的并查集虽然也是使用数组进行数据关系的存储,
    5. 但总体思路上和初版的并查集是大相径庭的,
    6. 由于让数据造成了一棵比较奇怪的树结构,更准确的说是森林结构,
    7. 在这个森林中每一棵树相应的节点之间的关系都是孩子指向父亲的,
    8. 这样一来能够经过任意的节点很是容易的查询到这棵树相应的根节点是谁,
    9. 那么相应的就知道了对于每个节点来讲它所属的集合编号是谁。
  2. 两个版本的并查集的性能
    1. 第一个版本的并查集 QuickFind,
    2. isConnected:判断两个集合是否链接 对应时间复杂度是O(1)级别的,
    3. union:将两个集合进行合并 对应时间复杂度是O(n)级别的。
    4. 第二个版本的并查集 QuickUnion,
    5. isConnected:判断两个集合是否链接 对应时间复杂度是O(h)级别的,
    6. union:将两个集合进行合并 对应时间复杂度是O(h)级别的。
  3. 在测试算法性能时候
    1. 不少时候实际测试的结果不只仅和算法有关,
    2. 也和你使用的语言具体执行的时候底层运行的机制相关,
    3. 第一个版本的并查集 总体就是使用的一个数组,
    4. 合并的操做就是对一片连续的空间进行一次循环的操做,
    5. 比方说这样的操做在 一些强类型的 语言的底层会有很是好的优化,
    6. 因此运行速度会很是快。
    7. 而第二个版本的并查集 查询的过程实际上是不断索引的过程,
    8. 它不是顺次的不断访问一片连续的空间,它要在不一样的地址之间进行跳转,
    9. 所以它的速度就会相对的慢一些,
    10. 并且在第二个版本的并查集中 find 的复杂度是O(h)级别的,
    11. 不管是 isConnected 仍是 union 都须要进行调用,
    12. 也就是说在第二个版本的并查集中的 isConnected 时间复杂度要比
    13. 第一个版本的并查集的 isConnected 时间复杂度要高的,
    14. 也就是更加的慢一些。
    15. 在第二个并查集中,
    16. 当你 union 的次数变得很大的时候,实际上就是将更多的元素组合在了一个集合中,
    17. 因此你获得的那棵树很是的大,可能仍是一个退化的超长链表,
    18. 那么它相应的深度可能就会很是的高,
    19. 这就会使得 isConnected 的操做时的消耗也会很是的高,
    20. 因此可能会让第二个版本的并查集明明是O(h)级别的复杂度还比
    21. 第一个版本的并查集的O(n)级别的复杂度还要慢一些,
    22. 因此第二个版本的并查集仍是有很大的优化空间的。
  4. 优化第二个版本的并查集
    1. 这个优化空间主要在于,在进行 union 操做的时候,
    2. 就直接将 q 这个元素的根节点直接去指向了 p 这个元素的根节点,
    3. 可是没有充分的考虑 q 和 p 这两个元素它所在的那两棵树的特色是怎样的,
    4. 若是不对要合并的那两个元素所在的树的形状不去作判断,
    5. 不少时候这个合并的过程会不断的增长树的高度,
    6. 甚至在一些极端的状况下获得的这棵树是一条链表的样子。
  5. 简单的解决方案:考虑 size
    1. 去考虑当前这棵树它总体有多少个节点,
    2. 也就是让节点少的那棵树去指向节点多的那棵树,
    3. 这样就高几率的让造成的那棵树它的深度相对的会比较低,
    4. 这个优化的思路实际上是很是简单的。
    5. 并且确定不会退化为一个链表,
    6. 由于能够保证最后造成的那棵树相对是比较浅的,
    7. 对于O(h)的时间复杂度来讲,h 越小它的时间复杂就会越小,
    8. 这样的简单优化让性能有了巨大的提高。
    9. 可是还能够继续进行优化。

代码示例

  1. (class: MyUnionFindOne, class: MyUnionFindTwo, class: MyUnionFindThree, class: PerformanceTest, class: Main)

  2. MyUnionFindOne

    // 自定义并查集 UnionFind 第一个版本 QuickFind版
    // isConnected 操做很快
    class MyUnionFindOne {
       constructor(size) {
          // 存储数据所对应的集合的编号
          this.ids = new Array(size);
    
          // 模拟存入数据
          const len = this.ids.length;
          for (var i = 0; i < len; i++) this.ids[i] = i;
       }
    
       // 功能:将元素q和元素p这两个数据以及他们所在的集合进行合并
       // 时间复杂度:O(n)
       unionElements(q, p) {
          const qId = this.find(q);
          const pId = this.find(p);
    
          if (qId === pId) return;
    
          for (var i = 0; i < this.ids.length; i++)
             if (pId === this.ids[i]) this.ids[i] = qId;
       }
    
       // 功能:查询元素q和元素p这两个数据是否在同一个集合中
       // 时间复杂度:O(1)
       isConnected(q, p) {
          return this.ids[q] === this.ids[p];
       }
    
       // 查找元素所对应的集合编号
       find(index) {
          if (index < 0 || index >= this.ids.length)
             throw new Error('index is out of bound.');
          return this.ids[index];
       }
    
       // 功能:当前并查集一共考虑多少个元素
       getSize() {
          return this.ids.length;
       }
    }
    复制代码
  3. MyUnionFindTwo

    // 自定义并查集 UnionFind 第二个版本 QuickUnion版
    // Union 操做变快了
    // 还能够更快的
    class MyUnionFindTwo {
       constructor(size) {
          // 存储当前节点所指向的父节点
          this.forest = new Array(size);
    
          // 在初始的时候每个节点都指向它本身
          // 也就是每个节点都是独立的一棵树
          const len = this.forest.length;
          for (var i = 0; i < len; i++) this.forest[i] = i;
       }
    
       // 功能:将元素q和元素p这两个数据以及他们所在的集合进行合并
       // 时间复杂度:O(h) h 为树的高度
       unionElements(treePrimary, treeSecondary) {
          const primaryRoot = this.find(treePrimary);
          const secondarRoot = this.find(treeSecondary);
    
          if (primaryRoot === secondarRoot) return;
    
          // 不管哪棵树往那棵树上进行合并 都同样,他们都是树
          // 这里是主树节点上往次树节点进行合并
          this.forest[primaryRoot] = this.forest[secondarRoot];
       }
    
       // 功能:查询元素q和元素p这两个数据是否在同一个集合中
       // 时间复杂度:O(h) h 为树的高度
       isConnected(treeQ, treeP) {
          return this.find(treeQ) === this.find(treeP);
       }
    
       // 查找元素所对应的集合编号
       find(id) {
          if (id < 0 || id >= this.forest.length)
             throw new Error('index is out of bound.');
    
          // 不断的去查查找当前节点的根节点
          // 根节点的索引是指向本身,若是根节点为 1 那么对应的索引也为 1。
          while (id !== this.forest[id]) id = this.forest[id];
    
          return id;
       }
    
       // 功能:当前并查集一共考虑多少个元素
       getSize() {
          return this.forest.length;
       }
    }
    复制代码
  4. MyUnionFindThree

    // 自定义并查集 UnionFind 第三个版本 QuickUnion优化版
    // Union 操做变快了
    // 还能够更快的
    // 解决方案:考虑size 也就是某一棵树从根节点开始一共有多少个节点
    // 原理:节点少的向节点多的树进行融合
    // 还能够更快的
    class MyUnionFindThree {
       constructor(size) {
          // 存储当前节点所指向的父节点
          this.forest = new Array(size);
          // 以以某个节点为根的全部子节点的个数
          this.branch = new Array(size);
    
          // 在初始的时候每个节点都指向它本身
          // 也就是每个节点都是独立的一棵树
          const len = this.forest.length;
          for (var i = 0; i < len; i++) {
             this.forest[i] = i;
             this.branch[i] = 1; // 默认节点个数为1
          }
       }
    
       // 功能:将元素q和元素p这两个数据以及他们所在的集合进行合并
       // 时间复杂度:O(h) h 为树的高度
       unionElements(treePrimary, treeSecondary) {
          const primaryRoot = this.find(treePrimary);
          const secondarRoot = this.find(treeSecondary);
    
          if (primaryRoot === secondarRoot) return;
    
          // 节点少的 树 往 节点多的树 进行合并,在必定程度上减小最终树的高度
          if (this.branch[primaryRoot] < this.branch[secondarRoot]) {
             // 主树节点上往次树节点进行合并
             this.forest[primaryRoot] = this.forest[secondarRoot];
             // 次树的节点个数 += 主树的节点个数
             this.branch[secondarRoot] += this.branch[primaryRoot];
          } else {
             // branch[primaryRoot] >= branch[secondarRoot]
             // 次树节点上往主树节点进行合并
             this.forest[secondarRoot] = this.forest[primaryRoot];
             // 主树的节点个数 += 次树的节点个数
             this.branch[primaryRoot] += this.branch[secondarRoot];
          }
       }
    
       // 功能:查询元素q和元素p这两个数据是否在同一个集合中
       // 时间复杂度:O(h) h 为树的高度
       isConnected(treeQ, treeP) {
          return this.find(treeQ) === this.find(treeP);
       }
    
       // 查找元素所对应的集合编号
       find(id) {
          if (id < 0 || id >= this.forest.length)
             throw new Error('index is out of bound.');
    
          // 不断的去查查找当前节点的根节点
          // 根节点的索引是指向本身,若是根节点为 1 那么对应的索引也为 1。
          while (id !== this.forest[id]) id = this.forest[id];
    
          return id;
       }
    
       // 功能:当前并查集一共考虑多少个元素
       getSize() {
          return this.forest.length;
       }
    }
    复制代码
  5. PerformanceTest

    // 性能测试
    class PerformanceTest {
       constructor() {}
    
       // 对比队列
       testQueue(queue, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             queue.enqueue(random() * openCount);
          }
    
          while (!queue.isEmpty()) {
             queue.dequeue();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 对比栈
       testStack(stack, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             stack.push(random() * openCount);
          }
    
          while (!stack.isEmpty()) {
             stack.pop();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 对比集合
       testSet(set, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          let arr = [];
          let temp = null;
    
          // 第一遍测试
          for (var i = 0; i < openCount; i++) {
             temp = random();
             // 添加剧复元素,从而测试集合去重的能力
             set.add(temp * openCount);
             set.add(temp * openCount);
    
             arr.push(temp * openCount);
          }
    
          for (var i = 0; i < openCount; i++) {
             set.remove(arr[i]);
          }
    
          // 第二遍测试
          for (var i = 0; i < openCount; i++) {
             set.add(arr[i]);
             set.add(arr[i]);
          }
    
          while (!set.isEmpty()) {
             set.remove(arr[set.getSize() - 1]);
          }
    
          let endTime = Date.now();
    
          // 求出两次测试的平均时间
          let avgTime = Math.ceil((endTime - startTime) / 2);
    
          return this.calcTime(avgTime);
       }
    
       // 对比映射
       testMap(map, openCount) {
          let startTime = Date.now();
    
          let array = new MyArray();
          let random = Math.random;
          let temp = null;
          let result = null;
          for (var i = 0; i < openCount; i++) {
             temp = random();
             result = openCount * temp;
             array.add(result);
             array.add(result);
             array.add(result);
             array.add(result);
          }
    
          for (var i = 0; i < array.getSize(); i++) {
             result = array.get(i);
             if (map.contains(result)) map.add(result, map.get(result) + 1);
             else map.add(result, 1);
          }
    
          for (var i = 0; i < array.getSize(); i++) {
             result = array.get(i);
             map.remove(result);
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 对比堆 主要对比 使用heapify 与 不使用heapify时的性能
       testHeap(heap, array, isHeapify) {
          const startTime = Date.now();
    
          // 是否支持 heapify
          if (isHeapify) heap.heapify(array);
          else {
             for (const element of array) heap.add(element);
          }
    
          console.log('heap size:' + heap.size() + '\r\n');
          document.body.innerHTML += 'heap size:' + heap.size() + '<br /><br />';
    
          // 使用数组取值
          let arr = new Array(heap.size());
          for (let i = 0; i < arr.length; i++) arr[i] = heap.extractMax();
    
          console.log(
             'Array size:' + arr.length + ',heap size:' + heap.size() + '\r\n'
          );
          document.body.innerHTML +=
             'Array size:' +
             arr.length +
             ',heap size:' +
             heap.size() +
             '<br /><br />';
    
          // 检验一下是否符合要求
          for (let i = 1; i < arr.length; i++)
             if (arr[i - 1] < arr[i]) throw new Error('error.');
    
          console.log('test heap completed.' + '\r\n');
          document.body.innerHTML += 'test heap completed.' + '<br /><br />';
    
          const endTime = Date.now();
          return this.calcTime(endTime - startTime);
       }
    
       // 对比并查集
       testUnionFind(unionFind, openCount, primaryArray, secondaryArray) {
          const size = unionFind.getSize();
          const random = Math.random;
    
          return this.testCustomFn(function() {
             // 合并操做
             for (var i = 0; i < openCount; i++) {
                let primaryId = primaryArray[i];
                let secondaryId = secondaryArray[i];
    
                unionFind.unionElements(primaryId, secondaryId);
             }
    
             // 查询链接操做
             for (var i = 0; i < openCount; i++) {
                let primaryRandomId = Math.floor(random() * size);
                let secondaryRandomId = Math.floor(random() * size);
    
                unionFind.unionElements(primaryRandomId, secondaryRandomId);
             }
          });
       }
    
       // 计算运行的时间,转换为 天-小时-分钟-秒-毫秒
       calcTime(result) {
          //获取距离的天数
          var day = Math.floor(result / (24 * 60 * 60 * 1000));
    
          //获取距离的小时数
          var hours = Math.floor((result / (60 * 60 * 1000)) % 24);
    
          //获取距离的分钟数
          var minutes = Math.floor((result / (60 * 1000)) % 60);
    
          //获取距离的秒数
          var seconds = Math.floor((result / 1000) % 60);
    
          //获取距离的毫秒数
          var milliSeconds = Math.floor(result % 1000);
    
          // 计算时间
          day = day < 10 ? '0' + day : day;
          hours = hours < 10 ? '0' + hours : hours;
          minutes = minutes < 10 ? '0' + minutes : minutes;
          seconds = seconds < 10 ? '0' + seconds : seconds;
          milliSeconds =
             milliSeconds < 100
                ? milliSeconds < 10
                   ? '00' + milliSeconds
                   : '0' + milliSeconds
                : milliSeconds;
    
          // 输出耗时字符串
          result =
             day +
             '天' +
             hours +
             '小时' +
             minutes +
             '分' +
             seconds +
             '秒' +
             milliSeconds +
             '毫秒' +
             ' <<<<============>>>> 总毫秒数:' +
             result;
    
          return result;
       }
    
       // 自定义对比
       testCustomFn(fn) {
          let startTime = Date.now();
    
          fn();
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    }
    复制代码
  6. Main

    // main 函数
    class Main {
       constructor() {
          this.alterLine('UnionFind Comparison Area');
          // 十万级别
          const size = 100000; // 并查集维护节点数
          const openCount = 100000; // 操做数
    
          // 生成同一份测试数据的辅助代码
          const random = Math.random;
          const primaryArray = new Array(openCount);
          const secondaryArray = new Array(openCount);
    
          // 生成同一份测试数据
          for (var i = 0; i < openCount; i++) {
             primaryArray[i] = Math.floor(random() * size);
             secondaryArray[i] = Math.floor(random() * size);
          }
    
          // 开始测试
          const myUnionFindOne = new MyUnionFindOne(size);
          const myUnionFindTwo = new MyUnionFindTwo(size);
          const myUnionFindThree = new MyUnionFindThree(size);
          const performanceTest = new PerformanceTest();
    
          // 测试后获取测试信息
          const myUnionFindOneInfo = performanceTest.testUnionFind(
             myUnionFindOne,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindTwoInfo = performanceTest.testUnionFind(
             myUnionFindTwo,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindThreeInfo = performanceTest.testUnionFind(
             myUnionFindThree,
             openCount,
             primaryArray,
             secondaryArray
          );
    
          // 总毫秒数:24143
          console.log(
             'MyUnionFindOne time:' + myUnionFindOneInfo,
             myUnionFindOne
          );
          this.show('MyUnionFindOne time:' + myUnionFindOneInfo);
          // 总毫秒数:32050
          console.log(
             'MyUnionFindTwo time:' + myUnionFindTwoInfo,
             myUnionFindTwo
          );
          this.show('MyUnionFindTwo time:' + myUnionFindTwoInfo);
          // 总毫秒数:69
          console.log(
             'MyUnionFindThree time:' + myUnionFindThreeInfo,
             myUnionFindThree
          );
          this.show('MyUnionFindThree time:' + myUnionFindThreeInfo);
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 页面加载完毕
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码

并查集 Quick Union 基于 Rank 的优化

  1. 这个 rank 就是指树的高度或树的深度
    1. 之因此不叫作 height 和 depth,
    2. 是由于进行路径压缩的时候并不会维护这个 rank 了,
    3. rank 只在 union 中进行维护,
    4. 这个 rank 准确的来讲只是一个粗略的排名或者序而已,
    5. 并非很准确的存储了树的高度或深度。
  2. rank 的优化是基于 size 优化的基础上进行的
    1. 最好的优化方式是记录每个节点的根节点的最大深度是多少,
    2. 这样才可以在合并的时候,
    3. 让深度比较低的那棵树向深度比较高的那棵树进行合并,
    4. 这样总体更加的合理,这样的一种优化方案就称之为 rank 的优化,
    5. 这个 rank 依然可使用一个数组来进行记录,
    6. 其中rank[i]表示根节点为 i 的树的高度是多少。
  3. rank 的优化性能其实和 size 优化的性能差不了多少
    1. 可是当数据量达到千万这个程度的时候,
    2. 就会有一点差距了,差距也不是有点大,就一两秒左右。
    3. 因此仍是有优化空间的。

代码示例

  1. (class: MyUnionFindThree, class: MyUnionFindFour, class: PerformanceTest, class: Main)

  2. MyUnionFindThree

    // 自定义并查集 UnionFind 第三个版本 QuickUnion优化版
    // Union 操做变快了
    // 还能够更快的
    // 解决方案:考虑size 也就是某一棵树从根节点开始一共有多少个节点
    // 原理:节点少的向节点多的树进行融合
    // 还能够更快的
    class MyUnionFindThree {
       constructor(size) {
          // 存储当前节点所指向的父节点
          this.forest = new Array(size);
          // 以以某个节点为根的全部子节点的个数
          this.branch = new Array(size);
    
          // 在初始的时候每个节点都指向它本身
          // 也就是每个节点都是独立的一棵树
          const len = this.forest.length;
          for (var i = 0; i < len; i++) {
             this.forest[i] = i;
             this.branch[i] = 1; // 默认节点个数为1
          }
       }
    
       // 功能:将元素q和元素p这两个数据以及他们所在的集合进行合并
       // 时间复杂度:O(h) h 为树的高度
       unionElements(treePrimary, treeSecondary) {
          const primaryRoot = this.find(treePrimary);
          const secondarRoot = this.find(treeSecondary);
    
          if (primaryRoot === secondarRoot) return;
    
          // 节点少的 树 往 节点多的树 进行合并,在必定程度上减小最终树的高度
          if (this.branch[primaryRoot] < this.branch[secondarRoot]) {
             // 主树节点上往次树节点进行合并
             this.forest[primaryRoot] = this.forest[secondarRoot];
             // 次树的节点个数 += 主树的节点个数
             this.branch[secondarRoot] += this.branch[primaryRoot];
          } else {
             // branch[primaryRoot] >= branch[secondarRoot]
             // 次树节点上往主树节点进行合并
             this.forest[secondarRoot] = this.forest[primaryRoot];
             // 主树的节点个数 += 次树的节点个数
             this.branch[primaryRoot] += this.branch[secondarRoot];
          }
       }
    
       // 功能:查询元素q和元素p这两个数据是否在同一个集合中
       // 时间复杂度:O(h) h 为树的高度
       isConnected(treeQ, treeP) {
          return this.find(treeQ) === this.find(treeP);
       }
    
       // 查找元素所对应的集合编号
       find(id) {
          if (id < 0 || id >= this.forest.length)
             throw new Error('index is out of bound.');
    
          // 不断的去查查找当前节点的根节点
          // 根节点的索引是指向本身,若是根节点为 1 那么对应的索引也为 1。
          while (id !== this.forest[id]) id = this.forest[id];
    
          return id;
       }
    
       // 功能:当前并查集一共考虑多少个元素
       getSize() {
          return this.forest.length;
       }
    }
    复制代码
  3. MyUnionFindFour

    // 自定义并查集 UnionFind 第四个版本 QuickUnion优化版
    // Union 操做变快了
    // 还能够更快的
    // 解决方案:考虑rank 也就是某一棵树从根节点开始计算最大深度是多少
    // 原理:让深度比较低的那棵树向深度比较高的那棵树进行合并
    // 还能够更快的
    class MyUnionFindFour {
       constructor(size) {
          // 存储当前节点所指向的父节点
          this.forest = new Array(size);
          // 记录某个节点为根的树的最大高度或深度
          this.rank = new Array(size);
    
          // 在初始的时候每个节点都指向它本身
          // 也就是每个节点都是独立的一棵树
          const len = this.forest.length;
          for (var i = 0; i < len; i++) {
             this.forest[i] = i;
             this.rank[i] = 1; // 默认深度为1
          }
       }
    
       // 功能:将元素q和元素p这两个数据以及他们所在的集合进行合并
       // 时间复杂度:O(h) h 为树的高度
       unionElements(treePrimary, treeSecondary) {
          const primaryRoot = this.find(treePrimary);
          const secondarRoot = this.find(treeSecondary);
    
          if (primaryRoot === secondarRoot) return;
    
          // 根据两个元素所在树的rank不一样判断合并方向
          // 将rank低的集合合并到rank高的集合上
          if (this.rank[primaryRoot] < this.rank[secondarRoot]) {
             // 主树节点上往次树节点进行合并
             this.forest[primaryRoot] = this.forest[secondarRoot];
          } else if (this.rank[primaryRoot] > this.rank[secondarRoot]) {
             // 次树节点上往主树节点进行合并
             this.forest[secondarRoot] = this.forest[primaryRoot];
          } else {
             // rank[primaryRoot] == rank[secondarRoot]
             // 若是元素个数同样的根节点,那谁指向谁都无所谓
             // 本质都是同样的
    
             // primaryRoot合并到secondarRoot上了,qRoot的高度就会增长1
             this.forest[primaryRoot] = this.forest[secondarRoot];
             this.rank[secondarRoot] += 1;
          }
       }
    
       // 功能:查询元素q和元素p这两个数据是否在同一个集合中
       // 时间复杂度:O(h) h 为树的高度
       isConnected(treeQ, treeP) {
          return this.find(treeQ) === this.find(treeP);
       }
    
       // 查找元素所对应的集合编号
       find(id) {
          if (id < 0 || id >= this.forest.length)
             throw new Error('index is out of bound.');
    
          // 不断的去查查找当前节点的根节点
          // 根节点的索引是指向本身,若是根节点为 1 那么对应的索引也为 1。
          while (id !== this.forest[id]) id = this.forest[id];
    
          return id;
       }
    
       // 功能:当前并查集一共考虑多少个元素
       getSize() {
          return this.forest.length;
       }
    }
    复制代码
  4. PerformanceTest

    // 性能测试
    class PerformanceTest {
       constructor() {}
    
       // 对比队列
       testQueue(queue, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             queue.enqueue(random() * openCount);
          }
    
          while (!queue.isEmpty()) {
             queue.dequeue();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 对比栈
       testStack(stack, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             stack.push(random() * openCount);
          }
    
          while (!stack.isEmpty()) {
             stack.pop();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 对比集合
       testSet(set, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          let arr = [];
          let temp = null;
    
          // 第一遍测试
          for (var i = 0; i < openCount; i++) {
             temp = random();
             // 添加剧复元素,从而测试集合去重的能力
             set.add(temp * openCount);
             set.add(temp * openCount);
    
             arr.push(temp * openCount);
          }
    
          for (var i = 0; i < openCount; i++) {
             set.remove(arr[i]);
          }
    
          // 第二遍测试
          for (var i = 0; i < openCount; i++) {
             set.add(arr[i]);
             set.add(arr[i]);
          }
    
          while (!set.isEmpty()) {
             set.remove(arr[set.getSize() - 1]);
          }
    
          let endTime = Date.now();
    
          // 求出两次测试的平均时间
          let avgTime = Math.ceil((endTime - startTime) / 2);
    
          return this.calcTime(avgTime);
       }
    
       // 对比映射
       testMap(map, openCount) {
          let startTime = Date.now();
    
          let array = new MyArray();
          let random = Math.random;
          let temp = null;
          let result = null;
          for (var i = 0; i < openCount; i++) {
             temp = random();
             result = openCount * temp;
             array.add(result);
             array.add(result);
             array.add(result);
             array.add(result);
          }
    
          for (var i = 0; i < array.getSize(); i++) {
             result = array.get(i);
             if (map.contains(result)) map.add(result, map.get(result) + 1);
             else map.add(result, 1);
          }
    
          for (var i = 0; i < array.getSize(); i++) {
             result = array.get(i);
             map.remove(result);
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 对比堆 主要对比 使用heapify 与 不使用heapify时的性能
       testHeap(heap, array, isHeapify) {
          const startTime = Date.now();
    
          // 是否支持 heapify
          if (isHeapify) heap.heapify(array);
          else {
             for (const element of array) heap.add(element);
          }
    
          console.log('heap size:' + heap.size() + '\r\n');
          document.body.innerHTML += 'heap size:' + heap.size() + '<br /><br />';
    
          // 使用数组取值
          let arr = new Array(heap.size());
          for (let i = 0; i < arr.length; i++) arr[i] = heap.extractMax();
    
          console.log(
             'Array size:' + arr.length + ',heap size:' + heap.size() + '\r\n'
          );
          document.body.innerHTML +=
             'Array size:' +
             arr.length +
             ',heap size:' +
             heap.size() +
             '<br /><br />';
    
          // 检验一下是否符合要求
          for (let i = 1; i < arr.length; i++)
             if (arr[i - 1] < arr[i]) throw new Error('error.');
    
          console.log('test heap completed.' + '\r\n');
          document.body.innerHTML += 'test heap completed.' + '<br /><br />';
    
          const endTime = Date.now();
          return this.calcTime(endTime - startTime);
       }
    
       // 对比并查集
       testUnionFind(unionFind, openCount, primaryArray, secondaryArray) {
          const size = unionFind.getSize();
          const random = Math.random;
    
          return this.testCustomFn(function() {
             // 合并操做
             for (var i = 0; i < openCount; i++) {
                let primaryId = primaryArray[i];
                let secondaryId = secondaryArray[i];
    
                unionFind.unionElements(primaryId, secondaryId);
             }
    
             // 查询链接操做
             for (var i = 0; i < openCount; i++) {
                let primaryRandomId = Math.floor(random() * size);
                let secondaryRandomId = Math.floor(random() * size);
    
                unionFind.unionElements(primaryRandomId, secondaryRandomId);
             }
          });
       }
    
       // 计算运行的时间,转换为 天-小时-分钟-秒-毫秒
       calcTime(result) {
          //获取距离的天数
          var day = Math.floor(result / (24 * 60 * 60 * 1000));
    
          //获取距离的小时数
          var hours = Math.floor((result / (60 * 60 * 1000)) % 24);
    
          //获取距离的分钟数
          var minutes = Math.floor((result / (60 * 1000)) % 60);
    
          //获取距离的秒数
          var seconds = Math.floor((result / 1000) % 60);
    
          //获取距离的毫秒数
          var milliSeconds = Math.floor(result % 1000);
    
          // 计算时间
          day = day < 10 ? '0' + day : day;
          hours = hours < 10 ? '0' + hours : hours;
          minutes = minutes < 10 ? '0' + minutes : minutes;
          seconds = seconds < 10 ? '0' + seconds : seconds;
          milliSeconds =
             milliSeconds < 100
                ? milliSeconds < 10
                   ? '00' + milliSeconds
                   : '0' + milliSeconds
                : milliSeconds;
    
          // 输出耗时字符串
          result =
             day +
             '天' +
             hours +
             '小时' +
             minutes +
             '分' +
             seconds +
             '秒' +
             milliSeconds +
             '毫秒' +
             ' <<<<============>>>> 总毫秒数:' +
             result;
    
          return result;
       }
    
       // 自定义对比
       testCustomFn(fn) {
          let startTime = Date.now();
    
          fn();
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    }
    复制代码
  5. Main

    // main 函数
    class Main {
       constructor() {
          this.alterLine('UnionFind Comparison Area');
          // 千万级别
          const size = 10000000; // 并查集维护节点数
          const openCount = 10000000; // 操做数
    
          // 生成同一份测试数据的辅助代码
          const random = Math.random;
          const primaryArray = new Array(openCount);
          const secondaryArray = new Array(openCount);
    
          // 生成同一份测试数据
          for (var i = 0; i < openCount; i++) {
             primaryArray[i] = Math.floor(random() * size);
             secondaryArray[i] = Math.floor(random() * size);
          }
    
          // 开始测试
          const myUnionFindThree = new MyUnionFindThree(size);
          const myUnionFindFour = new MyUnionFindFour(size);
          const performanceTest = new PerformanceTest();
    
          // 测试后获取测试信息
          const myUnionFindThreeInfo = performanceTest.testUnionFind(
             myUnionFindThree,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindFourInfo = performanceTest.testUnionFind(
             myUnionFindFour,
             openCount,
             primaryArray,
             secondaryArray
          );
    
          // 总毫秒数:8042
          console.log(
             'MyUnionFindThree time:' + myUnionFindThreeInfo,
             myUnionFindThree
          );
          this.show('MyUnionFindThree time:' + myUnionFindThreeInfo);
          // 总毫秒数:7463
          console.log(
             'MyUnionFindFour time:' + myUnionFindFourInfo,
             myUnionFindFour
          );
          this.show('MyUnionFindFour time:' + myUnionFindFourInfo);
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 页面加载完毕
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码
相关文章
相关标签/搜索