【从蛋壳到满天飞】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

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

并查集 路径压缩 Path compression

  1. 并查集的一个很是重要的优化 路径压缩算法

    1. 如下三种方式都是彻底同样的,
    2. 均可以表示这五个节点是相互链接的,
    3. 也就是说这三种方式是等效的,
    4. 在具体的查询过程当中,不管是调用 find 仍是 isConnected,
    5. 在这三种不一样的方式查询这五个节点中任意两个节点都是相链接的,
    6. 可是因为这三种树它们的深度不一样,因此效率是存在不一样的,
    7. 显然第一种树的高度达到了 5,因此执行 find(4)这个操做,
    8. 那相应的时间性能会相对的慢一些,而第三种树它的高度只有 2,
    9. 在这棵树中 find 任意一个节点它响应的时间性能就会比较高,
    10. 在以前实现的 union 中,是让根节点去指向另一个根节点,
    11. 这样的一个过程免不了构建出来的树愈来愈高,
    12. 路径压缩所解决的问题就是让一棵比较高的树可以压缩成为一棵比较矮的树,
    13. 对于并查集来讲每个节点的子树的个数是没有限制的,
    14. 因此最理想的状况下其实但愿每一棵树都是直接指向某一个根节点,
    15. 也就是说这个树它只有两层,根节点在第一层,其它的全部的节点都在第二层,
    16. 达到这种最理想的状况可能相对比较困难,因此退而求其次,
    17. 只要可以让这棵树的高度下降,那么对整个并查集的总体性能都是好的。
    //// 第一种链接方式 的树
    // (0)
    // /
    // (1)
    // /
    // (2)
    // /
    // (3)
    // /
    //(4)
    
    //// 第二种链接方式 的树
    // (0)
    // / \
    //(1) (2)
    // / \
    // (3) (4)
    
    //// 第三种链接方式 的树
    // (0)
    // / | \ \
    //(1)(2)(3)(4)
    复制代码
  2. 路径压缩api

    1. 路径压缩是发生在执行 find 这个操做中,也就是查找一个节点对应的根节点的过程当中,
    2. 须要从这个节点不断的向上直到找到这个根节点,那么能够在寻找的这个过程当中,
    3. 顺便让这个节点的深度下降,顺便进行路径压缩的过程,
    4. 只须要在向上遍历的时候同时执行parent[p] = parent[parent[p]]
    5. 也就是将 p 这个节点的父亲设置成这个节点父亲的父亲,
    6. 这样一来每次执行 find 都会让你的树下降高度,
    7. 以下图,整棵树原来的深度为 5,通过一轮遍历后,
    8. 深度降到了 3,这个过程就叫作路径压缩,在查询节点 4 的时候,
    9. 顺便整棵树的结构改变,让它的深度更加的浅了,
    10. 路径压缩是并查集这种数据结构相对比较经典,
    11. 也是比较广泛的一种优化思路,
    12. 在算法竞赛中一般实现并查集都要添加上路径压缩这样的优化。
    // // 原来的树是这个样子
    // (0)
    // /
    // (1)
    // /
    // (2)
    // /
    // (3)
    // /
    // (4)
    
    // // 执行一次find(4) 使用了 parent[p] = parent[parent[p]]
    // (0)
    // /
    // (1)
    // |
    // (2)
    // / \
    // (3) (4)
    
    // // 而后再从2开始向上遍历 再使用 parent[p] = parent[parent[p]]
    // (0)
    // / \
    // (1) (2)
    // / \
    // (3) (4)
    
    // 最后数组就是这个样子
    // 0 1 2 3 4
    // -----------------
    // prent 0 0 0 2 2
    复制代码
  3. 这个 rank 就是指树的高度或树的深度数组

    1. 之因此不叫作 height 和 depth,
    2. 是由于进行路径压缩的时候并不会维护这个 rank 了,
    3. 每个节点都在 rank 中记录了
    4. 以这个节点 i 为根的这个集合所表示的这棵树相应的层数,
    5. 在路径压缩的过程当中,节点的层数其实发生了改变,
    6. 不过并无这 find 中去维护 rank 数组,
    7. 这么作是合理的,这就是为何管这个数组叫作 rank
    8. 而不叫作深度 depth 或高度 height 的缘由,
    9. 它实际在添加上路径压缩这样的一个优化以后,
    10. 就再也不表示当前这个节点的高度或者是深度了,
    11. rank 这个词就是排名或者序的意思,
    12. 给每个节点其实相应的都有这样一个排名,
    13. 当你添加上了路径压缩以后,
    14. 依然是这个 rank 值相对比较低的这些节点在下面,
    15. rank 值相对比较高的节点在上面,
    16. 只不过可能出现同层的节点它们的 rank 值其实是不一样的,
    17. 不过它们总体之间的大小关系依然是存在的,
    18. 因此 rank 值只是做为 union 合并操做的时候进行的一个参考,
    19. 它依然能够胜任这样的一个参考的工做,
    20. 可是它并不实际反应每个节点所对应的那个高度值或者深度值,
    21. 实际上就算你不作这样的一个 rank 维护也是性能上的考虑,
    22. 若是要想把每个节点的具体高度或者深度维护住,
    23. 相应的性能消耗是比较高的,在整个并查集的使用过程当中,
    24. 其实对于每个节点很是精准的知道这个阶段所处的高度或者深度是多少,
    25. 并无必要那样去作,
    26. 使用这样一个比较粗略的 rank 值就能够彻底胜任整个并查集运行的工做了。

代码示例

  1. (class: MyUnionFindThree, class: MyUnionFindFour, class: MyUnionFindFive, class: PerformanceTest, class: Main)数据结构

  2. MyUnionFindThreedom

    // 自定义并查集 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. MyUnionFindFive

    // 自定义并查集 UnionFind 第五个版本 QuickUnion优化版
    // Union 操做变快了
    // 解决方案:考虑path compression 路径
    // 原理:在find的时候,循环遍历操做时,让当前节点的父节点指向它父亲的父亲。
    // 还能够更快的
    class MyUnionFindFive {
       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]) {
             // 进行一次节点压缩。
             this.forest[id] = this.forest[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 = 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 myUnionFindFive = new MyUnionFindFive(size);
          const performanceTest = new PerformanceTest();
    
          // 测试后获取测试信息
          const myUnionFindThreeInfo = performanceTest.testUnionFind(
             myUnionFindThree,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindFourInfo = performanceTest.testUnionFind(
             myUnionFindFour,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindFiveInfo = performanceTest.testUnionFind(
             myUnionFindFive,
             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);
          // 总毫秒数:5118
          console.log(
             'MyUnionFindFive time:' + myUnionFindFiveInfo,
             myUnionFindFive
          );
          this.show('MyUnionFindFive time:' + myUnionFindFiveInfo);
       }
    
       // 将内容显示在页面上
       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();
    };
    复制代码

更多和并查集相关的话题

  1. 路径压缩还能够继续优化

    1. 能够将树压缩的只剩下最后两层,
    2. 可是实现到这样的样子就须要借助递归来实现了,
    3. 查询某一个节点的时候,直接让当前这个节点以及以前全部的节点,
    4. 所有直接指向根节点。
    // // 原来的树是这个样子
    // (0)
    // /
    // (1)
    // /
    // (2)
    // /
    // (3)
    // /
    // (4)
    
    // 你能够优化成这个样子
    // (0)
    // / | \ \
    // (1)(2)(3)(4)
    
    // 最后数组就是这个样子
    // 0 1 2 3 4
    // -----------------
    // prent 0 0 0 0 0
    复制代码
  2. 非递归实现的路径压缩要比递归实现的路径压缩相对来讲快一点点

    1. 由于递归的过程是会有相应的开销的,因此相对会慢一点,
    2. 可是第五版的非递归实现的路径压缩也能够作到递归实现的路径压缩
    3. 这样直接让当前节点及全部的节点指向根节点,只不过是不能一次性的作到,
    4. 第五版的路径压缩下图这样的,若是在深度为 3 的树上再调用一下find(4)
    5. 就会变成第三个树结构的样子,它须要多调用几回find(4)
    6. 可是最终依然可以达到这样的一个结果,若是再调用一下find(3)
    7. 那么就会变成最后的和第六版递归同样的样子,
    8. 此时全部的节点都会指向根节点,
    9. 也就是说所制做的第五版的路径压缩也可以达到第六版路径压缩的效果,
    10. 只不过须要多调用几回,再加上第五版的路径压缩没有使用递归函数实现,
    11. 而是直接在循环遍历中实现的,因此总体性能会高一点点。
    // // 原来的树是这个样子
    // (0)
    // /
    // (1)
    // /
    // (2)
    // /
    // (3)
    // /
    // (4)
    
    // 优化成这个样子了
    // (0)
    // / \
    // (1) (2)
    // / \
    // (3) (4)
    
    // 再调用一下find(4),就会变成这个样子
    // (0)
    // / | \
    // (1)(2) (4)
    // /
    // (3)
    
    // 再调用一下find(3),就优化成这个样子
    // (0)
    // / | \ \
    // (1)(2)(3)(4)
    
    // 最后数组就是这个样子
    // 0 1 2 3 4
    // -----------------
    // prent 0 0 0 0 0
    复制代码

代码示例

  1. (class: MyUnionFindThree, class: MyUnionFindFour, class: MyUnionFindFive,

    1. class: MyUnionFindSix, 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. MyUnionFindFive

    // 自定义并查集 UnionFind 第五个版本 QuickUnion优化版
    // Union 操做变快了
    // 解决方案:考虑path compression 路径
    // 原理:在find的时候,循环遍历操做时,让当前节点的父节点指向它父亲的父亲。
    // 还能够更快的
    class MyUnionFindFive {
       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]) {
             // 进行一次节点压缩。
             this.forest[id] = this.forest[this.forest[id]];
             id = this.forest[id];
          }
    
          return id;
       }
    
       // 功能:当前并查集一共考虑多少个元素
       getSize() {
          return this.forest.length;
       }
    }
    复制代码
  5. MyUnionFindSix

    // 自定义并查集 UnionFind 第六个版本 QuickUnion优化版
    // Union 操做变快了
    // 解决方案:考虑path compression 路径
    // 原理:在find的时候,循环遍历操做时,让全部的节点都指向根节点 以递归的形式进行。
    // 还能够更快的
    class MyUnionFindSix {
       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.');
    
          // 若是当前节点不等于根节点,
          // 就找到根节点而且把当前节点及以前的节点所有指向根节点
          if (id !== this.forest[id])
             this.forest[id] = this.find(this.forest[id]);
    
          return this.forest[id];
       }
    
       // 功能:当前并查集一共考虑多少个元素
       getSize() {
          return this.forest.length;
       }
    }
    复制代码
  6. 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);
       }
    }
    复制代码
  7. 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 myUnionFindFive = new MyUnionFindFive(size);
          const myUnionFindSix = new MyUnionFindSix(size);
          const performanceTest = new PerformanceTest();
    
          // 测试后获取测试信息
          const myUnionFindThreeInfo = performanceTest.testUnionFind(
             myUnionFindThree,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindFourInfo = performanceTest.testUnionFind(
             myUnionFindFour,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindFiveInfo = performanceTest.testUnionFind(
             myUnionFindFive,
             openCount,
             primaryArray,
             secondaryArray
          );
          const myUnionFindSixInfo = performanceTest.testUnionFind(
             myUnionFindSix,
             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);
          // 总毫秒数:5118
          console.log(
             'MyUnionFindFive time:' + myUnionFindFiveInfo,
             myUnionFindFive
          );
          this.show('MyUnionFindFive time:' + myUnionFindFiveInfo);
          // 总毫秒数:5852
          console.log(
             'MyUnionFindSix time:' + myUnionFindSixInfo,
             myUnionFindSix
          );
          this.show('MyUnionFindSix time:' + myUnionFindSixInfo);
       }
    
       // 将内容显示在页面上
       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();
    };
    复制代码

并查集的时间复杂度分析

  1. 在并查集使用了这样一个奇怪的树结构来实现之后,
    1. 其实并查集的时间复杂度就是O(h)
    2. 不管是查询操做仍是合并操做它的时间复杂度都是O(h)这个级别的,
    3. 这个 h 就是树的高度或者深度,可是这个复杂度并不能反映 h 和 n 之间的关系,
    4. 对于并查集来讲它并非一个严格的二叉树、三叉树、几叉树,
    5. 因此这个 h 并非严格意义上 logn 的级别,
    6. 对于并查集的时间复杂度分析总体在数学上相对比较复杂。
  2. 严格意义上来说使用了路径压缩以后
    1. 并查集相应的时间复杂度,不管是查询操做仍是合并操做,
    2. 都是O(log*n)这个级别,这个 log*n 是另一个函数,
    3. 它和 log 函数不同,相应的log*的英文叫作iterated logarithm
    4. 也能够直接读成 log star,这个log*n在数学上有一个公式,
    5. log*n= {0 if(n<=1) || 1+log*(logn) if(n>1)}
    6. 也就是当n<=1的时候,log*n为 0,
    7. n>1的时候,稍微有点复杂了,这是一个递归的定义,
    8. 这个log*n = 1 + log*(logn),括号中就是对这个n取一个log值,
    9. 再来看这个log值对应的log*的这个是多少,
    10. 直到这个括号中logn获得的结果小于等于 1 了,那么就直接获得了 0,
    11. 这样递归的定义就到底了,这就是 log*n 这个公式的数学意义,
    12. 这也就证实了加入了路径压缩以后,
    13. 对于并查集的时间复杂度为何是O(log*n)这个级别的,
    14. 就会稍微有些复杂,只须要了解便可,
    15. log*n这样的时间复杂度能够经过以上公式能够看出,
    16. 它是一个比 logn 还要快的这样一个时间复杂度,总体上近乎是O(1)级别的,
    17. 因此它比O(1)稍微要慢一点点,其实 logn 已是很是快的一个时间复杂度了,
    18. 那么当并查集添加上了路径压缩以后,
    19. 平均来说查询操做和合并操做是比 logn 这个级别还要快的,
    20. 这就是由于在路径压缩以后每个节点都直接指向了根节点,
    21. 近乎每一次查询都只须要看一次就能够直接找到这个节点所对应的根节点是谁,
    22. 这就是并查集的时间复杂度。

leetcode 中并查集相应的问题

  1. leetcode 并查集题库
    1. https://leetcode-cn.com/tag/union-find/
    2. 这些问题不是中等就是困难的题目,
    3. 若是只是参加面试的话,在算法面试中考察的并查集几率很低很低的,
    4. 若是是参加竞赛的话,在一些竞赛的问题中可能会使用上并查集,
    5. 对于 leetcode 中的问题,不只仅是使用并查集能够解决的,
    6. 对于不少问题可使用图论中的相应的寻路算法或者
    7. 是求连通份量的方式直接进行解决,
    8. 也能够回答并查集单独回答的这样的一个链接问题的结果,
    9. 可是对于有一些问题来讲不可是高效的并且是有它独特的优点的,
    10. 尤为是对于这个问题来讲,
    11. 相应的数据之间的合并以及查询这两个操做是交替进行的,
    12. 它们是一个动态的过程,在这种时候并查集是能够发挥最大的优点。
    13. 这些题目是有难度,若是没有算法竞赛的经验,会花掉不少的时间。

四种树结构

  1. 并查集是一种很是奇怪的树结构
    1. 它是一种由孩子指向父亲这样的一种树结构。
  2. 四个处理不一样的问题的树结构
    1. 这些都是树结构的变种,
    2. 分别是 堆、线段树、Trie 字典树、并查集。
  3. 二分搜索树是最为普通的树结构。
    1. 以前本身实现的二分搜索树有一个很大的问题,
    2. 它可能会退化成为一个链表,
    3. 须要经过新的机制来避免这个问题的发生,
    4. 也就是让二分搜索树能够作到自平衡,
    5. 使得它不会退化成一个链表,
    6. 其实这种能够保持二分搜索树是自平衡的数据结构有不少,
    7. 最为经典的,同时也是在历史上最先实现的能够达到自平衡的二分搜索树,
    8. AVL 树。
相关文章
相关标签/搜索