【从蛋壳到满天飞】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 (一个一个的工程)java

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

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

集合和映射 Set And Map

  1. 集合和映射是高层的数据结构,
    1. 高层的数据结构还有栈和队列,
    2. 这种数据结构更像是定义好了这种数据结构的相应的使用接口,
    3. 有了这些使用的接口包括这些数据结构自己所维持的一些性质,
    4. 就能够很是容易的把它们放入一些具体的应用中,
    5. 可是底层实现能够是多种多样的,
    6. 好比栈和队列的底层实现便可以是动态数组也能够是链表,
    7. 集合 Set 和映射 Map 也是相似这样的数据结构。

集合-基于二分搜索树的实现

  1. 集合就是承载元素的一个容器
    1. 在集合中有一个很是重要的特色,
    2. 也就是每一个元素只能存在一次,
    3. 在具体应用的时候须要这样的数据结构,
    4. 它可以帮助你很是快速的进行去重这个工做,
    5. 去重指的是去除全部重复的元素,让全部的元素只保留一份,
    6. 例如你想统计一个饭馆有多少位会员,
    7. 这时候你就须要进行一个去重的操做,会员不可以重复,
    8. 不管是新客户仍是老客户都只能手持一张会员卡。
  2. 在二分搜索树的添加操做的时候
    1. 最开始实现的时候是不能盛放重复元素的,
    2. 因此这个二分搜索树自己
    3. 就是一个很是好的实现“集合”的底层数据结构,

集合接口

  1. MySet
    1. void add (e) : 不能添加剧复元素
    2. void remove (e)
    3. boolean conatains (e)
    4. int getSize ()
    5. boolean isEmpty ()
  2. 使用 MyBSTSet 来实现这个集合的接口

集合的应用

  1. 典型的应用:用于客户的统计
    1. 如你作一个网站,对访问的 ip 进行一个统计,
    2. 不只要关注总访问量,还要关注有多少不一样的 ip 访问,
    3. 或者今天跟昨天相比又有多少个新的 ip 来访问,
    4. 在这种时候就应该使用集合这种数据结构来作统计。
  2. 典型的应用:词汇量的统计
    1. 在进行英文阅读的时候你会去参考,这本书的词汇量究竟有多少,
    2. 对于一本书的词汇量来讲,相同的单词是只记一次的,
    3. 在这种时候就应该使用集合这种数据结构来作统计。

代码示例

  1. (class: MyBinarySearchTree, class: MyBSTSet, class: Main)c++

  2. MyBinarySearchTreegit

    // 自定义二分搜索树节点
    class MyBinarySearchTreeNode {
       constructor(element, left = null, right = null) {
          // 实际存储的元素
          this.element = element;
          // 当前节点的左子树
          this.left = left;
          // 当前节点的右子树
          this.right = right;
       }
    }
    
    // 自定义二分搜索树
    class MyBinarySearchTree {
       constructor() {
          this.root = null;
          this.size = 0;
       }
    
       // 添加元素到二分搜索树中 +
       add(element) {
          if (element === null) throw new Error("element is null. can't store.");
    
          this.root = this.recursiveAdd(this.root, element);
       }
    
       // 添加元素到二分搜索树中 递归算法 -
       recursiveAdd(node, newElement) {
          // 解决最基本的问题 也就是递归函数调用的终止条件
          if (node === null) {
             this.size++;
             return new MyBinarySearchTreeNode(newElement);
          }
    
          // 1. 当前节点的元素比新元素大
          // 那么新元素就会被添加到当前节点的左子树去
          // 2. 当前节点的元素比新元素小
          // 那么新元素就会被添加到当前节点的右子树去
          // 3. 当前节点的元素比新元素相等
          // 什么都不作了,由于目前不添加剧复的元素
          if (this.compare(node.element, newElement) > 0)
             node.left = this.recursiveAdd(node.left, newElement);
          else if (this.compare(node.element, newElement) < 0)
             node.right = this.recursiveAdd(node.right, newElement);
          else {
          }
    
          // 将复杂问题分解成多个性质相同的小问题,
          // 而后求出小问题的答案,
          // 最终构建出原问题的答案
          return node;
       }
    
       // 判断二分搜索树中是否包含某个元素 +
       contains(element) {
          if (this.root === null) throw new Error("root is null. can't query.");
    
          return this.recursiveContains(this.root, element);
       }
    
       // 判断二分搜索树种是否包含某个元素 递归算法 -
       recursiveContains(node, element) {
          if (node === null) return false;
    
          // 当前节点元素比 要搜索的元素 大
          if (this.compare(node.element, element) > 0)
             return this.recursiveContains(node.left, element);
          else if (this.compare(node.element, element) < 0)
             // 当前元素比 要搜索的元素 小
             return this.recursiveContains(node.right, element);
          // 两个元素相等
          else return true;
       }
    
       // 找到二分搜索树中的最大值的元素 +
       maximum() {
          if (this.size === 0) throw new Error('binary search tree is empty.');
    
          return this.recursiveMaximum(this.root).element;
       }
    
       // 找到二分搜索树中的最大值的元素的节点 递归算法 -
       recursiveMaximum(node) {
          // 解决最基本的问题 向右走再也走不动了,说明当前节点就是最大值节点。
          if (node.right === null) return node;
    
          return this.recursiveMaximum(node.right);
       }
    
       // 删除二分搜索树中最大值的元素的节点,并返回这个节点的元素 +
       removeMax() {
          let maxElement = this.maximum();
          this.root = this.recursiveRemoveMax(this.root);
          return maxElement;
       }
    
       // 删除二分搜索树中最大值的元素的节点,并返回这个节点 递归算法 -
       recursiveRemoveMax(node) {
          if (node.right === null) {
             // 先存 当前这个节点的左子树,
             // 由于可能当前这个节点仅仅没有右子树,只有左子树,
             // 那么左子树能够替代当前这个节点。
             let leftNode = node.left;
             node.left = null;
             this.size--;
    
             return leftNode;
          }
    
          node.right = this.recursiveRemoveMax(node.right);
          return node;
       }
    
       // 找到二分搜索树中的最小值 +
       minimum() {
          if (this.size === 0) throw new Error('binary search tree is empty.');
    
          return this.recursiveMinimum(this.root).element;
       }
    
       // 找到二分搜索树中的最小值的元素的节点 递归算法 -
       recursiveMinimum(node) {
          if (node.left === null) return node;
    
          return this.recursiveMinimum(node.left);
       }
    
       // 删除二分搜索树中最小值的元素的节点,并返回这个节点的元素 +
       removeMin() {
          let leftNode = this.minimum();
          this.root = this.recursiveRemoveMin(this.root);
          return leftNode;
       }
    
       // 删除二分搜索树中最小值的元素的节点,并返回这个节点 递归算法 -
       recursiveRemoveMin(node) {
          // 解决最简单的问题
          if (node.left === null) {
             let rightNode = node.right;
             node.right = null;
             this.size--;
             return rightNode;
          }
    
          // 将复杂的问题拆分为性质相同的小问题,
          // 而后求出这些小问题的解后构建出原问题的答案
          node.left = this.recursiveRemoveMin(node.left);
          return node;
       }
    
       // 删除二分搜索树上的任意节点
       remove(element) {
          this.root = this.recursiveRemove(this.root, element);
       }
    
       // 删除二分搜索树上的任意节点 递归算法
       // 返回删除对应元素节点后新的二分搜索树的根
       recursiveRemove(node, element) {
          if (node === null) return null;
    
          // 当前节点的元素值比待删除的元素小 那么就向当前节点的右子树中去找
          if (this.compare(node.element, element) < 0) {
             node.right = this.recursiveRemove(node.right, element);
             return node;
          } else if (this.compare(node.element, element) > 0) {
             // 向当前节点的左子树中去找
             node.left = this.recursiveRemove(node.left, element);
             return node;
          } else {
             // 若是找到了相同值的节点了,开始进行相应的处理
    
             // 若是这个节点左子树为空,那么就让这个节点的右子树覆盖当前节点
             if (node.left === null) {
                let rightNode = node.right;
                node.right = null;
                this.size--;
                return rightNode;
             }
    
             // 若是当前节点的右子树为空,那么就让这个节点的左子树覆盖当前节点
             if (node.right === null) {
                let leftNode = node.left;
                node.left = null;
                this.size--;
                return leftNode;
             }
    
             // 若是当前节点的左右子树都不为空,那么就开始特殊操做
             // 1. 先找到当前节点右子树上最小的那个节点,保存起来
             // 2. 而后删除掉当前节点右子树上最小的那个节点,
             // 3. 让保存起来的那个节点覆盖掉当前节点
             // 1. 也就是保存起来的那个节点的right = 删除掉当前节点右子树上最小的节点后返回的那个节点
             // 2. 再让保存起来的那个节点的left = 当前节点的left
             // 4. 解除当前节点及其left和right,全都赋值为null,这样就至关于把当前节点从二分搜索树中剔除了
             // 5. 返回保存的这个节点
    
             let successtor = this.recursiveMinimum(node.right);
             successtor.right = this.recursiveRemoveMin(node.right);
    
             // 恢复removeMin 操做的this.size -- 带来的影响
             this.size++;
    
             successtor.left = node.left;
    
             // 开始正式的删除当前节点的操做
             node = node.left = node.right = null;
             this.size--;
    
             // 返回当前保存的节点
             return successtor;
          }
       }
    
       // 前序遍历 +
       preOrder(operator) {
          this.recursivePreOrder(this.root, operator);
       }
    
       // 前序遍历 递归算法 -
       recursivePreOrder(node, operator) {
          if (node === null) return;
    
          // 调用一下操做方法
          operator(node.element);
          console.log(node, node.element);
    
          // 继续递归遍历左右子树
          this.recursivePreOrder(node.left, operator);
          this.recursivePreOrder(node.right, operator);
       }
    
       // 前序遍历 非递归算法 +
       nonRecursivePreOrder(operator) {
          let stack = new MyLinkedListStack();
          stack.push(this.root);
    
          let node = null;
          while (!stack.isEmpty()) {
             // 出栈操做
             node = stack.pop();
    
             operator(node.element); // 访问当前的节点
             console.log(node.element);
    
             // 栈是先入后出的,把须要后访问的节点 先放进去,先访问的节点后放进去
             // 前序遍历是访问当前节点,而后再遍历左子树,最后遍历右子树
             if (node.right !== null) stack.push(node.right);
             if (node.left !== null) stack.push(node.left);
          }
       }
    
       // 中序遍历 +
       inOrder(operator) {
          this.recursiveInOrder(this.root, operator);
       }
    
       // 中序遍历 递归算法 -
       recursiveInOrder(node, operator) {
          if (node == null) return;
    
          this.recursiveInOrder(node.left, operator);
    
          operator(node.element);
          console.log(node.element);
    
          this.recursiveInOrder(node.right, operator);
       }
    
       // 后序遍历 +
       postOrder(operator) {
          this.recursivePostOrder(this.root, operator);
       }
    
       // 后序遍历 递归算法 -
       recursivePostOrder(node, operator) {
          if (node == null) return;
    
          this.recursivePostOrder(node.left, operator);
          this.recursivePostOrder(node.right, operator);
    
          operator(node.element);
          console.log(node.element);
       }
    
       // 层序遍历
       levelOrder(operator) {
          let queue = new MyLinkedListQueue();
          queue.enqueue(this.root);
    
          let node = null;
          while (!queue.isEmpty()) {
             node = queue.dequeue();
    
             operator(node.element);
             console.log(node.element);
    
             // 队列 是先进先出的,因此从左往右入队
             // 栈 是后进先出的, 因此从右往左入栈
             if (node.left !== null) queue.enqueue(node.left);
    
             if (node.right !== null) queue.enqueue(node.right);
          }
       }
    
       // 获取二分搜索树中节点个数 +
       getSize() {
          return this.size;
       }
    
       // 返回二分搜索树是否为空的bool值 +
       isEmpty() {
          return this.size === 0;
       }
    
       // 新增一个比较的方法,专门用来比较新增的元素大小 -
       // 第一个元素比第二个元素大 就返回 1
       // 第一个元素比第二个元素小 就返回 -1
       // 第一个元素比第二个元素相等 就返回 0
       compare(elementA, elementB) {
          if (elementA === null || elementB === null)
             throw new Error("element is null. can't compare.");
    
          // 先直接写死
          if (elementA > elementB) return 1;
          else if (elementA < elementB) return -1;
          else return 0;
       }
    
       // 输出二分搜索树中的信息
       // @Override toString 2018-11-03-jwl
       toString() {
          let treeInfo = '';
          treeInfo += this.getBinarySearchTreeString(this.root, 0, treeInfo);
          return treeInfo;
       }
    
       // 写一个辅助函数,用来生成二分搜索树信息的字符串
       getBinarySearchTreeString(node, depth, treeInfo, pageContent = '') {
          //之前序遍历的方式
    
          if (node === null) {
             treeInfo += this.getDepthString(depth) + 'null \r\n';
    
             pageContent = this.getDepthString(depth) + 'null<br /><br />';
             document.body.innerHTML += `${pageContent}`;
    
             return treeInfo;
          }
    
          treeInfo += this.getDepthString(depth) + node.element + '\r\n';
    
          pageContent =
             this.getDepthString(depth) + node.element + '<br /><br />';
          document.body.innerHTML += `${pageContent}`;
    
          treeInfo = this.getBinarySearchTreeString(
             node.left,
             depth + 1,
             treeInfo
          );
          treeInfo = this.getBinarySearchTreeString(
             node.right,
             depth + 1,
             treeInfo
          );
    
          return treeInfo;
       }
    
       // 写一个辅助函数,用来生成递归深度字符串
       getDepthString(depth) {
          let depthString = '';
          for (var i = 0; i < depth; i++) {
             depthString += '-- ';
          }
          return depthString;
       }
    }
    复制代码
  3. MyBSTSetgithub

    // 自定义二分搜索树集合Set
    class MyBinarySearchTreeSet {
       constructor() {
          // 借用二分搜索树来实现这个接口
          this.myBinarySearchTree = new MyBinarySearchTree();
       }
    
       // 添加元素
       add(element) {
          this.myBinarySearchTree.add(element);
       }
    
       // 移除元素
       remove(element) {
          this.myBinarySearchTree.remove(element);
       }
    
       // 是否包含这个元素
       contains(element) {
          return this.myBinarySearchTree.contains(element);
       }
    
       // 遍历操做
       // 第一个参数 是回掉函数,
       // 第二个参数 是遍历的方式 深度优先遍历(前pre、中in、后post),广度优先遍历(层序level)
       each(operator, method) {
          // 遍历方式默认是非递归的前序遍历,
          // 其它的遍历方式就是递归的前、中、后、层序遍历。
          switch (method) {
             case 'pre':
                this.myBinarySearchTree.preOrder(operator);
                break;
             case 'in':
                this.myBinarySearchTree.inOrder(operator);
                break;
             case 'post':
                this.myBinarySearchTree.postOrder(operator);
                break;
             case 'level':
                this.myBinarySearchTree.levelOrder(operator);
                break;
             default:
                this.myBinarySearchTree.nonRecursivePreOrder(operator);
                break;
          }
       }
    
       // 获取集合中实际的元素个数
       getSize() {
          return this.myBinarySearchTree.getSize();
       }
    
       // 返回集合是否为空的bool值
       isEmpty() {
          return this.myBinarySearchTree.isEmpty();
       }
    }
    复制代码
  4. Main面试

    // main 函数
    class Main {
       constructor() {
          this.alterLine('MyBinarySearchTreeSet Area');
          {
             let n = 5;
             let set = new MyBinarySearchTreeSet();
    
             let random = Math.random;
             let temp = null;
             for (var i = 0; i < n; i++) {
                temp = random();
                set.add(n * n * n * temp);
                set.add(n * n * n * temp);
                set.add(n * n * n * temp);
                set.add(n * n * n * temp);
                set.add(n * n * n * temp);
                set.add(n * n * n * temp);
                set.add(n * n * n * temp);
             }
    
             console.log(set.getSize());
             this.show(set.getSize());
    
             let array = new MyArray(n);
             set.each(element => {
                console.log(element);
                this.show(element);
                array.add(element);
             });
    
             for (var i = 0; i < array.getSize(); i++) {
                set.remove(array.get(i));
             }
    
             console.log(set.getSize());
             this.show(set.getSize());
          }
       }
    
       // 将内容显示在页面上
       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. 那么也能够经过链表来实现集合。
  2. 二分搜索树和链表都属于动态数据结构数据库

    1. 对于二分搜索树来讲数据都是存储在一个一个 node 中,
    2. 链表也是把数据存储到一个一个的 node 中,
    3. 只不过这两个 node 的定义是不一样的,
    4. 对于二分搜索树来讲有左右两个指针来指向左子树和右子树,
    5. 而对于链表来讲每个 node 都指向了下一个 node,
    6. 因为它们一样是动态数据结构,
    7. 因此能够基于这两种数据结构为底层实现这个集合,
    8. 还能够相应的进行比较这两种数据结构实现后的性能,
    9. 经过它们的性能比较能够看出二分搜索树这种数据结构的优点所在。
    // 二分搜索树的Node
    class Node {
       e; // Element
       left; // Node
       right; // Node
    }
    
    // 链表的Node
    class Node {
       e; // Element
       next; // Node
    }
    复制代码

集合接口

  1. MySet
    1. void add (e) : 不能添加剧复元素
    2. void remove (e)
    3. boolean conatains (e)
    4. int getSize ()
    5. boolean isEmpty ()
  2. 使用 MyLinkedListSet 来实现这个集合的接口

代码示例

  1. ( class: MyLinkedList, class: MyLinkedListSet)

  2. MyLinkedList

    // 自定义链表节点
    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // 将一个数组对象 转换为一个链表 而且追加到当前节点上
       appendToLinkedListNode(array) {
          let head = null;
          if (this.element === null) {
             // 头部添加
             head = this;
             head.element = array[0];
             head.next = null;
          } else {
             // 插入式
             head = new MyLinkedListNode(array[0], null);
             head.next = this.next;
             this.next = head;
          }
    
          // 添加节点的方式 头部添加、尾部添加、中间插入
    
          // 尾部添加节点的方式
          for (var i = 1; i < array.length; i++) {
             head.next = new MyLinkedListNode(array[i], null);
             head = head.next;
          }
       }
    
       //@override
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    // 自定义链表
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 获取链表中实际的节点个数
       getSize() {
          return this.size;
       }
    
       // 判断链表是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在链表头添加节点
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虚拟头节点
          this.insert(0, element);
       }
    
       // 在链表指定索引处插入节点
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一个prev就是dummyHead
          let prev = this.dummyHead;
          // 以前变量i(索引)之因此要从 1 开始,由于索引为0的那个节点就是head,循环就不须要从0开始了,
          // 如今索引之因此要从 0 开始, 由于初始化时 多增长了一个虚拟的头节点
          // (由于这个索引为0的节点并非dummyHead,dummyHead这个节点并不记录为链表中的实际节点),
          // 小于index是由于要找到指定索引位置的前一个节点
          // 循环是由于 要继续找到指定索引处的节点的前一个节点
          for (var i = 0; i < index; i++) {
             // 不停的切换引用,直到找到对应索引处节点的下一个节点
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 扩展:在链表最后一个节点的位置添加节点
       addLast(element) {
          this.insert(this.size, element);
       }
    
       // 获取指定索引位置的元素
       get(index) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 若是你要找指定索引节点的前一个节点 就使用dummyHead
          // 若是你要找到指定索引节点 就使用dummyHead.next
          // 由于duumyHead并非第一个节点,由于它是一个虚拟节点,
          // dummyHead.next才是真正被记录的第一个节点。
          let node = this.dummyHead.next;
          for (var i = 0; i < index; i++) {
             node = node.next;
          }
    
          return node.element;
       }
    
       // 获取头节点的元素
       getFirst() {
          return this.get(0);
       }
    
       // 获取尾节点的元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // 设置指定索引位置的元素值
       set(index, element) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 从第一个真正被记录的节点开始,从0开始
          let node = this.dummyHead.next;
    
          // 索引为 0 时,实际上切换到的节点 它的索引为 1
          // i < index ,当索引为 index-1 时, 实际上切换到的节点 它的索引为index
          for (let i = 0; i < index; i++) {
             // 每一次切换 都只是改变引用
             // 不的在链表中找下一个节点
             node = node.next;
          }
    
          node.element = element;
       }
    
       // 全部节点中是否有包含该元素
       contains(element) {
          let node = this.dummyHead;
    
          while (node.next !== null) {
             if (node.next.element === element) return true;
             // 不停的向下切换
             node = node.next;
          }
    
          return false;
       }
    
       // 删除指定索引位置的节点
       remove(index) {
          // 验证索引的合法性
          if (index < 0 || index >= this.size) {
             throw new Error('remove error. index < 0 or index > this.size');
          }
    
          let node = this.dummyHead;
          for (let i = 0; i < index; i++) {
             node = node.next;
          }
    
          // 待删除的节点
          let delNode = node.next;
          // 给待删除那个节点的前一个的节点的next引用替换为
          // 但删除的这个节点的next
          node.next = delNode.next;
          // 或者这样也行
          // node.next = node.next.next;
    
          // 临时存储待删除的那个节点里的元素
          let element = delNode.element;
          // 清空 待删除的节点
          delNode = null;
          this.size--;
    
          return element;
       }
    
       // 扩展:移除链表头的元素
       removeFirst() {
          return this.remove(0);
       }
    
       // 扩展:移除链表尾部的元素
       removeLast() {
          return this.remove(this.size - 1);
       }
    
       // 输出链表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedList: size = ${this.size},\n`;
          arrInfo += `data = front [`;
          let node = this.dummyHead.next;
          while (node.next !== null) {
             arrInfo += `${node.element}->`;
             node = node.next;
          }
          arrInfo += 'NULL] tail';
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  3. MyLinkedListSet

    // 自定义链表集合Set
    class MyLinkedListSet {
       //
       constructor() {
          this.myLinkedList = new MyLinkedList();
       }
    
       add(element) {
          if (!this.myLinkedList.contains(element))
             this.myLinkedList.addFirst(element);
       }
    
       remove(element) {
          this.myLinkedList.removeElement(element);
       }
    
       contains(element) {
          return this.myLinkedList.contains(element);
       }
    
       each(operator) {
          let size = this.myLinkedList.getSize();
          for (var i = 0; i < size; i++) {
             operator(this.myLinkedList.get(i));
          }
       }
    
       getSize() {
          return this.myLinkedList.getSize();
       }
    
       isEmpty() {
          return this.myLinkedList.isEmpty();
       }
    }
    复制代码

集合类的复杂度分析

  1. 实现了两个基于动态数据结构的集合类
    1. 基于二分搜索树的集合类 MyBSTSet,
    2. 基于链表的集合类 MyLinkedListSet,
    3. 基于链表的集合类性能要差一些。
  2. 集合的时间复杂度分析
    1. 增长 add
    2. 查询 contains
    3. 删除 remove
  3. MyLinkedListSet 与 MyBSTSet 时间复杂度对比
    1. MyLinkedListSet 的时间复杂度为O(n)
    2. MyBSTSet 的时间复杂度为O(h) or O(log n)
    3. h = log2 (n+1) = O(log2 n)
    4. h 和 n 之间成一个 log 关系,log 以 2 为底的(n+1),
    5. 一般称它们之间的关系为O(log2 n)
    6. 也就是大 O log 以 2 为底 n 这样的一个关系,
    7. 在大 O 这样的一个定义下,这个底的大小能够忽略不计,
    8. 由于认为常数不重要,
    9. 以 2 为底、以 10 为底、以 100 为底,它都是一个 log 级别的函数,
    10. 就像看线性关系,前面的系数也是不关注的,
    11. 它是1*n、2*n、100*n、10000*n,它们都是线性的一个关系,
    12. 因此这个关系能够写成O(log n)
  4. 二分搜索树的局限性
    1. 虽然二分搜索树实现的集合时间复杂度为O(log n)
    2. 可是计算出这个时间复杂度是在满二叉树的状况下,
    3. 因此这个O(log n)实际上是一个最优的状况,
    4. 若是二叉树稍微有一些倾斜,也能达到O(log n)这个级别,
    5. 可是本身实现的二分搜索树有一个致命的问题,
    6. 它有最坏的状况,对于一样的数据,能够建立出不一样的二分搜索树。
    7. 例如每个节点的左孩子都为空只有右孩子,
    8. 在这种状况下二分搜索树和一个链表是同样的,
    9. 也就是说这棵二分搜索树的高度等于节点数,
    10. 这就是二分搜索树最坏的状况,例如按照顺序添加[1,2,3,4,5,6]
    11. 就能够建立出一颗退化成链表的二分搜索树,
    12. 在大多数实际状况下 MyBSTSet 不会那么奇怪的按照顺序添加数据,
    13. 由于那样会让二分搜索树退化成链表,
    14. 可是也有可能你的二分搜索树就会遇到最差的状况或者接近最差的状况,
    15. 那么此时你二分搜索树的性能近乎接近链表的性能,
    16. 这样的性能其实也是O(n)这样的性能,
    17. 这也是你以前实现的二分搜索树的局限性。
    18. 可是平均来说,大多数状况下它都能达到O(log n)的时间复杂度,
    19. 可是依然会有特殊状况,最差的状况他会退化成和链表同样的O(n)
    20. 因此就须要解决这个问题,
    21. 解决这个问题的方式就是建立所谓的平衡二叉树。
    22. 因此很是准确的说二分搜索树的时间复杂度为O(h)
    23. 在大多数状况下这个 h 等于log n,若是很是的不巧,那么这个 h 等于 n。
  5. logn 和 n 的差距
    1. n=16的时候,logn 让它取 2 为底,
    2. 相应的log2 n的值为 4,n 的值就是 16。
    3. 也就是说它们相差四倍,随着 n 的增大,
    4. 它们之间的差距就会愈来愈大。
    5. 好比说n=1024的时候,logn 仍是让它取 2 为底,
    6. 由于 2 的十次方为 1024,那么相应的log2 n的值为 10,
    7. n 的值就是 1024,在这种状况下,两者之间相差一百倍。
    8. 若是n=100万这个级别的数据的话,logn 仍是让它取 2 为底,
    9. 由于 2 的 20 次方大概就是 100 万,那么相应的log2 n的值为 20,
    10. n 的值就是 100 万,在这种状况下,两者之间相差 5 万倍。
  6. 它们之间的差距很是很是大
    1. 好比你使用O(log n)这个算法和O(n)这个算法,
    2. 输入的数据为 100 万个,
    3. 若是你O(log n)这个算法花一秒时间就跑完的话,
    4. 那么O(n)这个算法就须要花 5 万秒,大概 14 个小时,
    5. 再好比你O(log n)这个算法要花一天的时间跑完这个程序,
    6. 那么O(n)这个算法就须要花 5 万天,大概 137 年的事件才能跑出结果,
    7. 这概念就像你睡一觉 24 小时后O(log n)算法的程序就跑出结果了,
    8. O(n)算法的程序你这一生都跑不出结果来。
  7. logn 这个复杂度是很是快很是快的一个时间复杂度
    1. 不少高级的排序算法最终它是 nlogn 这个时间复杂度,
    2. 这个时间复杂度比 n 方的时间复杂度快了很是多倍,
    3. 快的倍数与O(log n)O(n)差很少,
    4. 虽然它们之间还有常数的差别,
    5. 可是在复杂度分析的时不去管这个常数的差别。

MyLinkedListSet

  1. 增长 add O(n)
    1. 为了防止元素重复,因此必须先查询一遍,
    2. 而后再决定添加不添加,虽然添加的复杂度为O(1)
    3. 可是查询的操做是遍历整个链表,因此总体时间复杂度为O(n)
  2. 查询 contains O(n)
    1. 查询的操做是遍历整个链表,
    2. 因此时间复杂度为O(n)
  3. 删除 remove O(n)
    1. 删操做也须要遍历整个链表,
    2. 因此时间复杂度为O(n)

MyBSTSet

  1. 增长 add O(h) or O(log n)
    1. 添加一个元素,
    2. 待添加的这个元素和根节点的这个元素进行比较,
    3. 若是小于的话直接去左子树,若是大于的话直接去右子树,
    4. 每一次近乎都能把一半儿的元素给扔掉,
    5. 添加这个元素这个过程其实就像是在走一个链表,
    6. 一层一层的从这个树的根节点向叶子节点出发,
    7. 最终一共经历的节点个数就是这棵树的高度
    8. 也就是整棵书最大的深度,查询元素也是如此,
    9. 删除元素仍是如此,因此对于二分搜索树来讲,
    10. 这三个时间复杂度都是O(h)这个级别的,
    11. 这个 h 就是二分搜索树的高度。
  2. 查询 contains O(h) or O(log n)
  3. 删除 remove O(h) or O(log n)

二分搜索树

  1. 满的二叉树每一层有多少个节点
    1. 第 0 层:1 个节点
    2. 第 1 层:2 个节点
    3. 第 2 层:4 个节点
    4. 第 3 层:8 个节点
    5. 第 4 层:16 个节点
    6. 第 h-1 层:2^(h-1)个节点
  2. 满的二叉树一共有多少个节点
    1. 能够经过等比数列来进行计算,
    2. 2^(1-1) + 2^(2-1) + ... + 2^(h-1)
    3. = 1 x (1-2^(h)) / (1-2) = 2^(h) - 1 = n
  3. 满的二叉树的高度与节点个数之间的关系
    1. h = log2 (n+1) = O(log2 n)
    2. h 和 n 之间成一个 log 关系,log 以 2 为底的(n+1),
    3. 一般称它们之间的关系为O(log2 n)
    4. 也就是大 O log 以 2 为底 n 这样的一个关系,
    5. 在大 O 这样的一个定义下,这个底的大小能够忽略不计,
    6. 由于认为常数不重要,
    7. 以 2 为底、以 10 为底、以 100 为底,它都是一个 log 级别的函数,
    8. 就像看线性关系,前面的系数也是不关注的,
    9. 它是1*n、2*n、100*n、10000*n,它们都是线性的一个关系,
    10. 因此这个关系能够写成O(log n)
  4. 二分搜索树这种底层的数据结构实现的集合
    1. 它性能是大大的快于链表实现的集合。

代码示例

  1. (class: MyLinkedList, class: MyBinarySearchTree, class: PerformanceTest,

    1. class: MyLinkedListSet, class:MyBSTSet , class: Main)
  2. MyLinkedList

    // 自定义链表节点
    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // 将一个数组对象 转换为一个链表 而且追加到当前节点上
       appendToLinkedListNode(array) {
          let head = null;
          if (this.element === null) {
             // 头部添加
             head = this;
             head.element = array[0];
             head.next = null;
          } else {
             // 插入式
             head = new MyLinkedListNode(array[0], null);
             head.next = this.next;
             this.next = head;
          }
    
          // 添加节点的方式 头部添加、尾部添加、中间插入
    
          // 尾部添加节点的方式
          for (var i = 1; i < array.length; i++) {
             head.next = new MyLinkedListNode(array[i], null);
             head = head.next;
          }
       }
    
       //@override
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    // 自定义链表
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 获取链表中实际的节点个数
       getSize() {
          return this.size;
       }
    
       // 判断链表是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在链表头添加节点
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虚拟头节点
          this.insert(0, element);
       }
    
       // 在链表指定索引处插入节点
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一个prev就是dummyHead
          let prev = this.dummyHead;
          // 以前变量i(索引)之因此要从 1 开始,由于索引为0的那个节点就是head,循环就不须要从0开始了,
          // 如今索引之因此要从 0 开始, 由于初始化时 多增长了一个虚拟的头节点
          // (由于这个索引为0的节点并非dummyHead,dummyHead这个节点并不记录为链表中的实际节点),
          // 小于index是由于要找到指定索引位置的前一个节点
          // 循环是由于 要继续找到指定索引处的节点的前一个节点
          for (var i = 0; i < index; i++) {
             // 不停的切换引用,直到找到对应索引处节点的下一个节点
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 扩展:在链表最后一个节点的位置添加节点
       addLast(element) {
          this.insert(this.size, element);
       }
    
       // 获取指定索引位置的元素
       get(index) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 若是你要找指定索引节点的前一个节点 就使用dummyHead
          // 若是你要找到指定索引节点 就使用dummyHead.next
          // 由于duumyHead并非第一个节点,由于它是一个虚拟节点,
          // dummyHead.next才是真正被记录的第一个节点。
          let node = this.dummyHead.next;
          for (var i = 0; i < index; i++) {
             node = node.next;
          }
    
          return node.element;
       }
    
       // 获取头节点的元素
       getFirst() {
          return this.get(0);
       }
    
       // 获取尾节点的元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // 设置指定索引位置的元素值
       set(index, element) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 从第一个真正被记录的节点开始,从0开始
          let node = this.dummyHead.next;
    
          // 索引为 0 时,实际上切换到的节点 它的索引为 1
          // i < index ,当索引为 index-1 时, 实际上切换到的节点 它的索引为index
          for (let i = 0; i < index; i++) {
             // 每一次切换 都只是改变引用
             // 不的在链表中找下一个节点
             node = node.next;
          }
    
          node.element = element;
       }
    
       // 全部节点中是否有包含该元素
       contains(element) {
          let node = this.dummyHead;
    
          while (node.next !== null) {
             if (node.next.element === element) return true;
             // 不停的向下切换
             node = node.next;
          }
    
          return false;
       }
    
       // 删除指定索引位置的节点
       remove(index) {
          // 验证索引的合法性
          if (index < 0 || index >= this.size) {
             throw new Error('remove error. index < 0 or index > this.size');
          }
    
          let node = this.dummyHead;
          for (let i = 0; i < index; i++) {
             node = node.next;
          }
    
          // 待删除的节点
          let delNode = node.next;
          // 给待删除那个节点的前一个的节点的next引用替换为
          // 但删除的这个节点的next
          node.next = delNode.next;
          // 或者这样也行
          // node.next = node.next.next;
    
          // 临时存储待删除的那个节点里的元素
          let element = delNode.element;
          // 清空 待删除的节点
          delNode = null;
          this.size--;
    
          return element;
       }
    
       // 扩展:移除链表头的元素
       removeFirst() {
          return this.remove(0);
       }
    
       // 扩展:移除链表尾部的元素
       removeLast() {
          return this.remove(this.size - 1);
       }
    
       // 新增:根据元素来删除链表中的元素 2018-11-05
       removeElement(element) {
          let prev = this.dummyHead;
    
          while (prev.next !== null) {
             if (prev.next.element === element) break;
             prev = prev.next;
          }
    
          if (prev.next !== null) {
             let delNode = prev.next;
             prev.next = delNode.next;
             delNode = null;
             this.size--;
          }
       }
    
       // 输出链表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedList: size = ${this.size},\n`;
          arrInfo += `data = front [`;
          let node = this.dummyHead.next;
          while (node.next !== null) {
             arrInfo += `${node.element}->`;
             node = node.next;
          }
          arrInfo += 'NULL] tail';
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  3. MyBinarySearchTree

    // 自定义二分搜索树节点
    class MyBinarySearchTreeNode {
       constructor(element, left = null, right = null) {
          // 实际存储的元素
          this.element = element;
          // 当前节点的左子树
          this.left = left;
          // 当前节点的右子树
          this.right = right;
       }
    }
    
    // 自定义二分搜索树
    class MyBinarySearchTree {
       constructor() {
          this.root = null;
          this.size = 0;
       }
    
       // 添加元素到二分搜索树中 +
       add(element) {
          if (element === null) throw new Error("element is null. can't store.");
    
          this.root = this.recursiveAdd(this.root, element);
       }
    
       // 添加元素到二分搜索树中 递归算法 -
       recursiveAdd(node, newElement) {
          // 解决最基本的问题 也就是递归函数调用的终止条件
          if (node === null) {
             this.size++;
             return new MyBinarySearchTreeNode(newElement);
          }
    
          // 1. 当前节点的元素比新元素大
          // 那么新元素就会被添加到当前节点的左子树去
          // 2. 当前节点的元素比新元素小
          // 那么新元素就会被添加到当前节点的右子树去
          // 3. 当前节点的元素比新元素相等
          // 什么都不作了,由于目前不添加剧复的元素
          if (this.compare(node.element, newElement) > 0)
             node.left = this.recursiveAdd(node.left, newElement);
          else if (this.compare(node.element, newElement) < 0)
             node.right = this.recursiveAdd(node.right, newElement);
          else {
          }
    
          // 将复杂问题分解成多个性质相同的小问题,
          // 而后求出小问题的答案,
          // 最终构建出原问题的答案
          return node;
       }
    
       // 判断二分搜索树中是否包含某个元素 +
       contains(element) {
          if (this.root === null) throw new Error("root is null. can't query.");
    
          return this.recursiveContains(this.root, element);
       }
    
       // 判断二分搜索树种是否包含某个元素 递归算法 -
       recursiveContains(node, element) {
          if (node === null) return false;
    
          // 当前节点元素比 要搜索的元素 大
          if (this.compare(node.element, element) > 0)
             return this.recursiveContains(node.left, element);
          else if (this.compare(node.element, element) < 0)
             // 当前元素比 要搜索的元素 小
             return this.recursiveContains(node.right, element);
          // 两个元素相等
          else return true;
       }
    
       // 找到二分搜索树中的最大值的元素 +
       maximum() {
          if (this.size === 0) throw new Error('binary search tree is empty.');
    
          return this.recursiveMaximum(this.root).element;
       }
    
       // 找到二分搜索树中的最大值的元素的节点 递归算法 -
       recursiveMaximum(node) {
          // 解决最基本的问题 向右走再也走不动了,说明当前节点就是最大值节点。
          if (node.right === null) return node;
    
          return this.recursiveMaximum(node.right);
       }
    
       // 删除二分搜索树中最大值的元素的节点,并返回这个节点的元素 +
       removeMax() {
          let maxElement = this.maximum();
          this.root = this.recursiveRemoveMax(this.root);
          return maxElement;
       }
    
       // 删除二分搜索树中最大值的元素的节点,并返回这个节点 递归算法 -
       recursiveRemoveMax(node) {
          if (node.right === null) {
             // 先存 当前这个节点的左子树,
             // 由于可能当前这个节点仅仅没有右子树,只有左子树,
             // 那么左子树能够替代当前这个节点。
             let leftNode = node.left;
             node.left = null;
             this.size--;
    
             return leftNode;
          }
    
          node.right = this.recursiveRemoveMax(node.right);
          return node;
       }
    
       // 找到二分搜索树中的最小值 +
       minimum() {
          if (this.size === 0) throw new Error('binary search tree is empty.');
    
          return this.recursiveMinimum(this.root).element;
       }
    
       // 找到二分搜索树中的最小值的元素的节点 递归算法 -
       recursiveMinimum(node) {
          if (node.left === null) return node;
    
          return this.recursiveMinimum(node.left);
       }
    
       // 删除二分搜索树中最小值的元素的节点,并返回这个节点的元素 +
       removeMin() {
          let leftNode = this.minimum();
          this.root = this.recursiveRemoveMin(this.root);
          return leftNode;
       }
    
       // 删除二分搜索树中最小值的元素的节点,并返回这个节点 递归算法 -
       recursiveRemoveMin(node) {
          // 解决最简单的问题
          if (node.left === null) {
             let rightNode = node.right;
             node.right = null;
             this.size--;
             return rightNode;
          }
    
          // 将复杂的问题拆分为性质相同的小问题,
          // 而后求出这些小问题的解后构建出原问题的答案
          node.left = this.recursiveRemoveMin(node.left);
          return node;
       }
    
       // 删除二分搜索树上的任意节点
       remove(element) {
          this.root = this.recursiveRemove(this.root, element);
       }
    
       // 删除二分搜索树上的任意节点 递归算法
       // 返回删除对应元素节点后新的二分搜索树的根
       recursiveRemove(node, element) {
          if (node === null) return null;
    
          // 当前节点的元素值比待删除的元素小 那么就向当前节点的右子树中去找
          if (this.compare(node.element, element) < 0) {
             node.right = this.recursiveRemove(node.right, element);
             return node;
          } else if (this.compare(node.element, element) > 0) {
             // 向当前节点的左子树中去找
             node.left = this.recursiveRemove(node.left, element);
             return node;
          } else {
             // 若是找到了相同值的节点了,开始进行相应的处理
    
             // 若是这个节点左子树为空,那么就让这个节点的右子树覆盖当前节点
             if (node.left === null) {
                let rightNode = node.right;
                node.right = null;
                this.size--;
                return rightNode;
             }
    
             // 若是当前节点的右子树为空,那么就让这个节点的左子树覆盖当前节点
             if (node.right === null) {
                let leftNode = node.left;
                node.left = null;
                this.size--;
                return leftNode;
             }
    
             // 若是当前节点的左右子树都不为空,那么就开始特殊操做
             // 1. 先找到当前节点右子树上最小的那个节点,保存起来
             // 2. 而后删除掉当前节点右子树上最小的那个节点,
             // 3. 让保存起来的那个节点覆盖掉当前节点
             // 1. 也就是保存起来的那个节点的right = 删除掉当前节点右子树上最小的节点后返回的那个节点
             // 2. 再让保存起来的那个节点的left = 当前节点的left
             // 4. 解除当前节点及其left和right,全都赋值为null,这样就至关于把当前节点从二分搜索树中剔除了
             // 5. 返回保存的这个节点
    
             let successtor = this.recursiveMinimum(node.right);
             successtor.right = this.recursiveRemoveMin(node.right);
    
             // 恢复removeMin 操做的this.size -- 带来的影响
             this.size++;
    
             successtor.left = node.left;
    
             // 开始正式的删除当前节点的操做
             node = node.left = node.right = null;
             this.size--;
    
             // 返回当前保存的节点
             return successtor;
          }
       }
    
       // 前序遍历 +
       preOrder(operator) {
          this.recursivePreOrder(this.root, operator);
       }
    
       // 前序遍历 递归算法 -
       recursivePreOrder(node, operator) {
          if (node === null) return;
    
          // 调用一下操做方法
          operator(node.element);
          console.log(node, node.element);
    
          // 继续递归遍历左右子树
          this.recursivePreOrder(node.left, operator);
          this.recursivePreOrder(node.right, operator);
       }
    
       // 前序遍历 非递归算法 +
       nonRecursivePreOrder(operator) {
          let stack = new MyLinkedListStack();
          stack.push(this.root);
    
          let node = null;
          while (!stack.isEmpty()) {
             // 出栈操做
             node = stack.pop();
    
             operator(node.element); // 访问当前的节点
             console.log(node.element);
    
             // 栈是先入后出的,把须要后访问的节点 先放进去,先访问的节点后放进去
             // 前序遍历是访问当前节点,而后再遍历左子树,最后遍历右子树
             if (node.right !== null) stack.push(node.right);
             if (node.left !== null) stack.push(node.left);
          }
       }
    
       // 中序遍历 +
       inOrder(operator) {
          this.recursiveInOrder(this.root, operator);
       }
    
       // 中序遍历 递归算法 -
       recursiveInOrder(node, operator) {
          if (node == null) return;
    
          this.recursiveInOrder(node.left, operator);
    
          operator(node.element);
          console.log(node.element);
    
          this.recursiveInOrder(node.right, operator);
       }
    
       // 后序遍历 +
       postOrder(operator) {
          this.recursivePostOrder(this.root, operator);
       }
    
       // 后序遍历 递归算法 -
       recursivePostOrder(node, operator) {
          if (node == null) return;
    
          this.recursivePostOrder(node.left, operator);
          this.recursivePostOrder(node.right, operator);
    
          operator(node.element);
          console.log(node.element);
       }
    
       // 层序遍历
       levelOrder(operator) {
          let queue = new MyLinkedListQueue();
          queue.enqueue(this.root);
    
          let node = null;
          while (!queue.isEmpty()) {
             node = queue.dequeue();
    
             operator(node.element);
             console.log(node.element);
    
             // 队列 是先进先出的,因此从左往右入队
             // 栈 是后进先出的, 因此从右往左入栈
             if (node.left !== null) queue.enqueue(node.left);
    
             if (node.right !== null) queue.enqueue(node.right);
          }
       }
    
       // 获取二分搜索树中节点个数 +
       getSize() {
          return this.size;
       }
    
       // 返回二分搜索树是否为空的bool值 +
       isEmpty() {
          return this.size === 0;
       }
    
       // 新增一个比较的方法,专门用来比较新增的元素大小 -
       // 第一个元素比第二个元素大 就返回 1
       // 第一个元素比第二个元素小 就返回 -1
       // 第一个元素比第二个元素相等 就返回 0
       compare(elementA, elementB) {
          if (elementA === null || elementB === null)
             throw new Error("element is null. can't compare.");
    
          // 先直接写死
          if (elementA > elementB) return 1;
          else if (elementA < elementB) return -1;
          else return 0;
       }
    
       // 输出二分搜索树中的信息
       // @Override toString 2018-11-03-jwl
       toString() {
          let treeInfo = '';
          treeInfo += this.getBinarySearchTreeString(this.root, 0, treeInfo);
          return treeInfo;
       }
    
       // 写一个辅助函数,用来生成二分搜索树信息的字符串
       getBinarySearchTreeString(node, depth, treeInfo, pageContent = '') {
          //之前序遍历的方式
    
          if (node === null) {
             treeInfo += this.getDepthString(depth) + 'null \r\n';
    
             pageContent = this.getDepthString(depth) + 'null<br /><br />';
             document.body.innerHTML += `${pageContent}`;
    
             return treeInfo;
          }
    
          treeInfo += this.getDepthString(depth) + node.element + '\r\n';
    
          pageContent =
             this.getDepthString(depth) + node.element + '<br /><br />';
          document.body.innerHTML += `${pageContent}`;
    
          treeInfo = this.getBinarySearchTreeString(
             node.left,
             depth + 1,
             treeInfo
          );
          treeInfo = this.getBinarySearchTreeString(
             node.right,
             depth + 1,
             treeInfo
          );
    
          return treeInfo;
       }
    
       // 写一个辅助函数,用来生成递归深度字符串
       getDepthString(depth) {
          let depthString = '';
          for (var i = 0; i < depth; i++) {
             depthString += '-- ';
          }
          return depthString;
       }
    }
    复制代码
  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);
       }
    
       // 计算运行的时间,转换为 天-小时-分钟-秒-毫秒
       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;
       }
    }
    复制代码
  5. MyLinkedListSet

    // 自定义链表集合Set
    class MyLinkedListSet {
       //
       constructor() {
          this.myLinkedList = new MyLinkedList();
       }
    
       add(element) {
          if (!this.myLinkedList.contains(element))
             this.myLinkedList.addFirst(element);
       }
    
       remove(element) {
          this.myLinkedList.removeElement(element);
       }
    
       contains(element) {
          return this.myLinkedList.contains(element);
       }
    
       each(operator) {
          let size = this.myLinkedList.getSize();
          for (var i = 0; i < size; i++) {
             operator(this.myLinkedList.get(i));
          }
       }
    
       getSize() {
          return this.myLinkedList.getSize();
       }
    
       isEmpty() {
          return this.myLinkedList.isEmpty();
       }
    }
    复制代码
  6. MyBSTSet

    // 自定义二分搜索树集合Set
    class MyBinarySearchTreeSet {
       constructor() {
          // 借用二分搜索树来实现这个接口
          this.myBinarySearchTree = new MyBinarySearchTree();
       }
    
       // 添加元素
       add(element) {
          this.myBinarySearchTree.add(element);
       }
    
       // 移除元素
       remove(element) {
          this.myBinarySearchTree.remove(element);
       }
    
       // 是否包含这个元素
       contains(element) {
          return this.myBinarySearchTree.contains(element);
       }
    
       // 遍历操做
       // 第一个参数 是回掉函数,
       // 第二个参数 是遍历的方式 深度优先遍历(前pre、中in、后post),广度优先遍历(层序level)
       each(operator, method) {
          // 遍历方式默认是非递归的前序遍历,
          // 其它的遍历方式就是递归的前、中、后、层序遍历。
          switch (method) {
             case 'pre':
                this.myBinarySearchTree.preOrder(operator);
                break;
             case 'in':
                this.myBinarySearchTree.inOrder(operator);
                break;
             case 'post':
                this.myBinarySearchTree.postOrder(operator);
                break;
             case 'level':
                this.myBinarySearchTree.levelOrder(operator);
                break;
             default:
                this.myBinarySearchTree.nonRecursivePreOrder(operator);
                break;
          }
       }
    
       // 获取集合中实际的元素个数
       getSize() {
          return this.myBinarySearchTree.getSize();
       }
    
       // 返回集合是否为空的bool值
       isEmpty() {
          return this.myBinarySearchTree.isEmpty();
       }
    }
    复制代码
  7. Main

    // main 函数
    class Main {
       constructor() {
          this.alterLine('Set Comparison Area');
          let myLinkedListSet = new MyLinkedListSet();
          let myBinarySearchTreeSet = new MyBinarySearchTreeSet();
          let performanceTest = new PerformanceTest();
    
          let myLinkedListSetInfo = performanceTest.testSet(
             myLinkedListSet,
             5000
          );
          let myBinarySearchTreeSetInfo = performanceTest.testSet(
             myBinarySearchTreeSet,
             5000
          );
    
          this.alterLine('MyLinkedListSet Area');
          console.log(myLinkedListSetInfo);
          this.show(myLinkedListSetInfo);
    
          this.alterLine('MyBinarySearchTreeSet Area');
          console.log(myBinarySearchTreeSetInfo);
          this.show(myBinarySearchTreeSetInfo);
       }
    
       // 将内容显示在页面上
       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. 在不少算法面试题来讲,集合也是有很大的做用的。
  2. 使用 内置的集合
    1. 内置的 Set 比本身实现的 MyBSTSet 要强大不少,
    2. 由于 底层实现 Set 的树结构是一个平衡二叉树,
    3. 更准确的说是基于红黑树来进行实现的,
    4. 因此这个 Set 不会出现最差的时间复杂度O(n)的状况,
    5. 在最差的状况下在 Set 中进行增删查也是O(logn)这种级别,
    6. 而且这个 Set 还定义了更多的操做,
    7. 这些操做都是和二分搜索树具备顺序性相关的操做。

解决 leetcode 上的集合问题

  1. 804.惟一摩尔斯密码词

  2. 网址:https://leetcode-cn.com/problems/unique-morse-code-words/

  3. 解答

    // 答题
    class Solution {
       // leetcode 804. 惟一摩尔斯密码词
       uniqueMorseRepresentations(words) {
          /** * @param {string[]} words * @return {number} * 使用本身的二分搜索树来实现 */
          var uniqueMorseRepresentations = function(words) {
             // 摩斯码
             const codes = [
                '.-',
                '-...',
                '-.-.',
                '-..',
                '.',
                '..-.',
                '--.',
                '....',
                '..',
                '.---',
                '-.-',
                '.-..',
                '--',
                '-.',
                '---',
                '.--.',
                '--.-',
                '.-.',
                '...',
                '-',
                '..-',
                '...-',
                '.--',
                '-..-',
                '-.--',
                '--..'
             ];
    
             const myBinarySearchTreeSet = new MyBinarySearchTreeSet();
             let content = '';
             // 获取起始字符的aceii码,
             // 从而能够求出某个单词的每个字符在字母表中占的位置索引,
             // 根据这些位置索引就能够在摩斯表中找到相应的摩斯码,
             // 一个单词就是一组摩斯码,而后使用set添加,就能够直接实现去重的操做了
             const start = 'a'.charCodeAt(0);
             for (const word of words) {
                for (const w of word) content += codes[w.charCodeAt(0) - start];
    
                myBinarySearchTreeSet.add(content);
                content = '';
             }
    
             return myBinarySearchTreeSet.getSize();
          };
    
          /** * @param {string[]} words * @return {number} * 使用系统内置的Set集合类 */
          var uniqueMorseRepresentations = function(words) {
             // 摩斯码
             const codes = [
                '.-',
                '-...',
                '-.-.',
                '-..',
                '.',
                '..-.',
                '--.',
                '....',
                '..',
                '.---',
                '-.-',
                '.-..',
                '--',
                '-.',
                '---',
                '.--.',
                '--.-',
                '.-.',
                '...',
                '-',
                '..-',
                '...-',
                '.--',
                '-..-',
                '-.--',
                '--..'
             ];
    
             const set = new Set();
             let content = '';
             // 获取起始字符的aceii码,
             // 从而能够求出某个单词的每个字符在字母表中占的位置索引,
             // 根据这些位置索引就能够在摩斯表中找到相应的摩斯码,
             // 一个单词就是一组摩斯码,而后使用set添加,就能够直接实现去重的操做了
             const start = 'a'.charCodeAt(0);
             for (const word of words) {
                for (const w of word) content += codes[w.charCodeAt(0) - start];
    
                set.add(content);
                content = '';
             }
    
             return set.size;
          };
    
          return uniqueMorseRepresentations(words);
       }
    }
    // main 函数
    class Main {
       constructor() {
          this.alterLine('leetcode 804.惟一摩尔斯密码词');
          let s = new Solution();
          let words = ['gin', 'zen', 'gig', 'msg'];
          this.show(s.uniqueMorseRepresentations(words));
       }
    
       // 将内容显示在页面上
       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. 它们本质都是有序的集合。
  2. 有序的集合是指元素在集合中是具备顺序性的
    1. 例如在二分搜索树中存储的元素,
    2. 能够很轻易地从小到大遍历出来或者
    3. 去看这个元素的是上或下一个元素是谁等等,
    4. 这个准确的来讲就是有序集合(OrderSet)。
  3. 无序的集合是指元素在集合中是没有顺序的
    1. 例如在链表中存储的元素,
    2. 只是根据元素插入的顺序来决定这些元素在集合中的顺序,
    3. 不可以轻易地从小到大来遍历在这个集合中全部的元素,
    4. 也没法很是容易的去找到这个集合中最小最大的元素是谁、
    5. 上或下一个元素谁等等这些操做。
  4. 一般有序的集合都是经过搜索树来实现的,
    1. 不管是二分搜索树仍是平衡二叉树它们都是搜索树,
    2. 由于搜索树就有这样的优点,它能够实现有序的集合,
    3. 对于有些问题集合的有序性是很是重要的,
    4. 在另外一些问题中彻底没有必要使用有序集合,
    5. 好比仅仅是处理放重复元素的问题上,根本利用不到集合的有序性,
    6. 彻底可使用无序的集合来解决这个问题。
  5. 对于无序的集合其实仍是有更好的解决方案的,
    1. 那就是基于哈希表的实现,对于哈希表来讲,
    2. 相应的增删查这样的操做其实比搜索树还要快,
    3. 其实对于搜索树的实现来讲若是它保持了有序性,
    4. 那么它的能力其实也会更大,这个能力就表如今很轻易的查询到最大最小元素,
    5. 或者某一个元素的前一个元素和后一个元素等等,
    6. 轻易完成这些操做是有代价的,
    7. 这个代价其实就在时间复杂性上,它是稍微差于哈希表的。

多重集合

  1. 对于集合来讲在大多数状况下是不但愿有重复元素的,
    1. 可是在有些状况下也但愿有集合能够容纳重复的元素,
    2. 在这种状况下就称之为多重集合(MultipleSet),
    3. 多重集合具体的实现也很是简单,
    4. 只须要在容许重复的二分搜索树上进行包装一下一下便可,
    5. 你所解决的问题是否须要使用多重集合是根据业务场景所决定的,
    6. 一般使用集合的大多数状况下仍是选择不包含重复元素的集合。

映射(Map)

  1. 高中数学里的函数就能够理解成是一种映射

    1. f(x)=2*x+1,在映域中每取出一个值,
    2. 相应的在值域中都有有一个值与它对应,
    3. 如 x 为 1,f(x)就为 3,x 为 2,f(x)就为 5,
    4. 从一个值向另一个值的对应关系其实就是映射。
  2. 映射关系你也能够把它称之为字典

    1. 如 单词 -----> 释意,
    2. 字典就是这样一个从单词对应到释意这种数据的一个集合,
    3. 字典的英文是 dictionary,
    4. 在不少语言中把映射这样的一种数据结构称之为 dictionary 的简写 dict,
    5. 最典型的就是 python 里面基础数据结构 dict,
    6. 可是在 java、c++、js 语言中把这种关系称之为 Map,
    7. 其实它描述的就是相似字典这样的数据结构。
  3. 生活中的映射的应用

    1. dict:key ----> value
    2. 字典:单词 ----> 释意
    3. 名册:身份证号 ----> 人
    4. 车辆管理:车牌号 ----> 车
    5. 数据库:id ----> 信息
    6. 词频统计:单词 ----> 频率
  4. 存储(键,值)数据对的数据结构(key,value)

    1. 数据是一对一对出现的,这样的数据结构就叫映射,
    2. 不少时候都是要根据键(Key)来寻找值(Value),
    3. 例如生活中的映射的应用例子。
  5. 能够很是容易的使用链表或者二分搜索树来实现映射。

    // 链表实现时的Node
       class Node {
          key; // Key
          value; //Value
          Node next;// Node
       }
    
       // 二分搜索树实现时的Node
       class Node {
          key; // Key
          value; //Value
          left;// Node
          right;// Node
       }
    复制代码

映射接口

  1. MyMap
    1. void add(k, v)
    2. V remove(k)
    3. boolean contains(k)
    4. V get(k)
    5. void set(k, v)
    6. int getSize()
    7. boolean isEmpty()

使用链表来实现映射 Map

代码示例

  1. (class: MyLinkedListMap)

  2. MyLinkedListMap

    // 自定义链表映射节点 LinkedListMapNode
    class MyLinkedListMapNode {
       constructor(key = null, value = null, next = null) {
          this.key = key;
          this.value = value;
          this.next = next;
       }
    
       // @Override toString 2018-11-5-jwl
       toString() {
          return this.key.toString() + '---------->' + this.value.toString();
       }
    }
    
    // 自定义链表映射 Map
    class MyLinkedListMap {
       constructor() {
          this.dummyHead = new MyLinkedListMapNode();
          this.size = 0;
       }
    
       // 根据key获取节点 -
       getNode(key) {
          let cur = this.dummyHead.next;
    
          while (cur !== null) {
             if (cur.key === key) return cur;
             cur = cur.next;
          }
    
          return null;
       }
    
       // 添加操做 +
       add(key, value) {
          let node = this.getNode(key);
          // 这个节点若是存在就 覆盖值便可
          if (node !== null) node.value = value;
          else {
             // 若是不存在,那么就在头部添加如下
             let newNode = new MyLinkedListMapNode(key, value);
             newNode.next = this.dummyHead.next;
             this.dummyHead.next = newNode;
             this.size++;
          }
       }
    
       // 删除操做 返回被删除的元素 +
       remove(key) {
          let prev = this.dummyHead;
          // 循环查找
          while (prev.next !== null) {
             if (prev.next.key === key) break;
             prev = prev.next;
          }
    
          // 若是触碰了break, 那就知足条件
          if (prev.next !== null) {
             let delNode = prev.next;
             prev.next = delNode.next;
    
             let value = delNode.value;
             devNode = delNode.next = null;
             this.size--;
             return value;
          }
    
          // 若是没有触屏break 那就返回空值回去
          return null;
       }
    
       // 查询操做 返回查询到的元素 +
       get(key) {
          let node = this.getNode(key);
          if (node === null) return null;
          return node.value;
       }
    
       // 修改操做 +
       set(key, value) {
          let node = this.getNode(key);
          if (node === null) throw new Error(key + " doesn't exist.");
    
          node.value = value;
       }
    
       // 返回是否包含该key的元素的判断值 +
       contains(key) {
          return this.getNode(key) !== null;
       }
    
       // 返回映射中实际的元素个数 +
       getSize() {
          return this.size;
       }
    
       // 返回映射中是否为空的判断值 +
       isEmpty() {
          return this.size === 0;
       }
    
       // @Override toString() 2018-11-05-jwl
       toString() {
          let mapInfo = `MyLinkedListMap: size = ${this.size}, data = [ `;
          document.body.innerHTML += `MyLinkedListMap: size = ${ this.size }, data = [ <br/><br/>`;
    
          let cur = this.dummyHead.next;
    
          for (var i = 0; i < this.size - 1; i++) {
             mapInfo += ` ${cur.toString()}, \r\n`;
             document.body.innerHTML += ` ${cur.toString()}, <br/><br/>`;
             cur = cur.next;
          }
    
          if (cur !== null) {
             mapInfo += ` ${cur.toString()} \r\n`;
             document.body.innerHTML += ` ${cur.toString()} <br/><br/>`;
          }
    
          mapInfo += ` ] \r\n`;
          document.body.innerHTML += ` ] <br/><br/>`;
    
          return mapInfo;
       }
    }
    复制代码

使用二分搜索树来实现映射 Map

代码示例

  1. (class: MyBSTMap)

  2. MyBSTMap

    // 自定义二分搜索树树映射节点 TreeMapNode
    class MyBinarySearchTreeMapNode {
       constructor(key = null, value = null, left = null, right = null) {
          this.key = key;
          this.value = value;
          this.left = left;
          this.right = right;
       }
    
       // @Override toString 2018-11-5-jwl
       toString() {
          return this.key.toString() + '---------->' + this.value.toString();
       }
    }
    
    // 自定义二分搜索树映射 Map
    class MyBinarySearchTreeMap {
       constructor() {
          this.root = null;
          this.size = 0;
       }
    
       // 比较的功能
       compare(keyA, keyB) {
          if (keyA === null || keyB === null)
             throw new Error("key is error. key can't compare.");
          if (keyA > keyB) return 1;
          else if (keyA < keyB) return -1;
          else return 0;
       }
    
       // 根据key获取节点 -
       getNode(node, key) {
          // 先解决最基本的问题
          if (node === null) return null;
    
          // 开始将复杂的问题 逐渐缩小规模
          // 从而求出小问题的解,最后构建出原问题的解
          switch (this.compare(node.key, key)) {
             case 1: // 向左找
                return this.getNode(node.left, key);
                break;
             case -1: // 向右找
                return this.getNode(node.right, key);
                break;
             case 0: // 找到了
                return node;
                break;
             default:
                throw new Error(
                   'compare result is error. compare result : 0、 一、 -1 .'
                );
                break;
          }
       }
    
       // 添加操做 +
       add(key, value) {
          this.root = this.recursiveAdd(this.root, key, value);
       }
    
       // 添加操做 递归算法 -
       recursiveAdd(node, key, value) {
          // 解决最简单的问题
          if (node === null) {
             this.size++;
             return new MyBinarySearchTreeMapNode(key, value);
          }
    
          // 将复杂的问题规模逐渐变小,
          // 从而求出小问题的解,从而构建出原问题的答案
          if (this.compare(node.key, key) > 0)
             node.left = this.recursiveAdd(node.left, key, value);
          else if (this.compare(node.key, key) < 0)
             node.right = this.recursiveAdd(node.right, key, value);
          else node.value = value;
    
          return node;
       }
    
       // 删除操做 返回被删除的元素 +
       remove(key) {
          let node = this.getNode(this.root, key);
          if (node === null) return null;
    
          this.root = this.recursiveRemove(this.root, key);
          return node.value;
       }
    
       // 删除操做 递归算法 +
       recursiveRemove(node, key) {
          // 解决最基本的问题
          if (node === null) return null;
    
          if (this.compare(node.key, key) > 0) {
             node.left = this.recursiveRemove(node.left, key);
             return node;
          } else if (this.compare(node.key, key) < 0) {
             node.right = this.recursiveRemove(node.right, key);
             return node;
          } else {
             // 当前节点的key 与 待删除的key的那个节点相同
             // 有三种状况
             // 1. 当前节点没有左子树,那么只有让当前节点的右子树直接覆盖当前节点,就表示当前节点被删除了
             // 2. 当前节点没有右子树,那么只有让当前节点的左子树直接覆盖当前节点,就表示当前节点被删除了
             // 3. 当前节点左右子树都有, 那么又分两种状况,使用前驱删除法或者后继删除法
             // 1. 前驱删除法:使用当前节点的左子树上最大的那个节点覆盖当前节点
             // 2. 后继删除法:使用当前节点的右子树上最小的那个节点覆盖当前节点
    
             if (node.left === null) {
                let rightNode = node.right;
                node.right = null;
                this.size--;
                return rightNode;
             } else if (node.right === null) {
                let leftNode = node.left;
                node.left = null;
                this.size--;
                return leftNode;
             } else {
                let predecessor = this.maximum(node.left);
                node.left = this.removeMax(node.left);
                this.size++;
    
                // 开始嫁接 当前节点的左右子树
                predecessor.left = node.left;
                predecessor.right = node.right;
    
                // 将当前节点从根节点剔除
                node = node.left = node.right = null;
                this.size--;
    
                // 返回嫁接后的新节点
                return predecessor;
             }
          }
       }
    
       // 删除操做的两个辅助函数
       // 获取最大值、删除最大值
       // 之前驱的方式 来辅助删除操做的函数
    
       // 获取最大值
       maximum(node) {
          // 不再能往右了,说明当前节点已是最大的了
          if (node.right === null) return node;
    
          // 将复杂的问题渐渐减少规模,从而求出小问题的解,最后用小问题的解构建出原问题的答案
          return this.maximum(node.right);
       }
    
       // 删除最大值
       removeMax(node) {
          // 解决最基本的问题
          if (node.right === null) {
             let leftNode = node.left;
             node.left = null;
             this.size--;
             return leftNode;
          }
    
          // 开始化归
          node.right = this.removeMax(node.right);
          return node;
       }
    
       // 查询操做 返回查询到的元素 +
       get(key) {
          let node = this.getNode(this.root, key);
          if (node === null) return null;
          return node.value;
       }
    
       // 修改操做 +
       set(key, value) {
          let node = this.getNode(this.root, key);
          if (node === null) throw new Error(key + " doesn't exist.");
    
          node.value = value;
       }
    
       // 返回是否包含该key的元素的判断值 +
       contains(key) {
          return this.getNode(this.root, key) !== null;
       }
    
       // 返回映射中实际的元素个数 +
       getSize() {
          return this.size;
       }
    
       // 返回映射中是否为空的判断值 +
       isEmpty() {
          return this.size === 0;
       }
    
       // @Override toString() 2018-11-05-jwl
       toString() {
          let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `;
          document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [ <br/><br/>`;
    
          // 以非递归的前序遍历 输出字符串
          let stack = new MyLinkedListStack();
    
          stack.push(this.root);
    
          if (this.root === null) stack.pop();
    
          while (!stack.isEmpty()) {
             let node = stack.pop();
    
             if (node.left !== null) stack.push(node.left);
             if (node.right !== null) stack.push(node.right);
    
             if (node.left === null && node.right === null) {
                mapInfo += ` ${node.toString()} \r\n`;
                document.body.innerHTML += ` ${node.toString()} <br/><br/>`;
             } else {
                mapInfo += ` ${node.toString()}, \r\n`;
                document.body.innerHTML += ` ${node.toString()}, <br/><br/>`;
             }
          }
    
          mapInfo += ` ] \r\n`;
          document.body.innerHTML += ` ] <br/><br/>`;
    
          return mapInfo;
       }
    }
    复制代码

两种映射 Map 的时间复杂度分析

MyLinkedListMap O(n)

  1. 增长 add O(n)
    1. 为了防止指定 key 的节点不存在,因此必须先查询一遍,
    2. 而后再决定是直接赋值仍是建立新节点,虽然添加的复杂度为O(1)
    3. 可是查询的操做是遍历整个链表,因此总体时间复杂度为O(n)
  2. 查询 contains、get O(n)
    1. 查询的操做是遍历整个链表,
    2. 因此时间复杂度为O(n)
  3. 修改 set O(n)
    1. 为了防止指定 key 的节点不存在,因此必须先查询一遍,
    2. 因此时间复杂度为O(n)
  4. 删除 remove O(n)
    1. 删操做也须要遍历整个链表,
    2. 因此时间复杂度为O(n)

MyBSTMap O(h) or O(log n)

  1. 增长 add O(h) or O(log n)
    1. 添加一个元素(key/value),
    2. 待添加的这个元素 key 和根节点的这个元素 key 进行比较,
    3. 若是小于的话直接去左子树,若是大于的话直接去右子树,
    4. 每一次近乎都能把一半儿的元素(key/value)给扔掉,
    5. 添加这个元素这个过程其实就像是在走一个链表,
    6. 一层一层的从这个树的根节点向叶子节点出发,
    7. 最终一共经历的节点个数就是这棵树的高度
    8. 也就是整棵书最大的深度,查询元素也是如此,
    9. 删除元素仍是如此,因此对于二分搜索树来讲,
    10. 这三个时间复杂度都是O(h)这个级别的,
    11. 这个 h 就是二分搜索树的高度。
  2. 查询 contains、get O(h) or O(log n)
  3. 修改 set O(h) or O(log n)
  4. 删除 remove O(h) or O(log n)

代码示例

  1. class: MyLinkedListMap, class: MyBSTMap , class: PerformanceTest, class: Main)

  2. MyLinkedListMap

    // 自定义链表映射节点 LinkedListMapNode
    class MyLinkedListMapNode {
       constructor(key = null, value = null, next = null) {
          this.key = key;
          this.value = value;
          this.next = next;
       }
    
       // @Override toString 2018-11-5-jwl
       toString() {
          return this.key.toString() + '---------->' + this.value.toString();
       }
    }
    
    // 自定义链表映射 Map
    class MyLinkedListMap {
       constructor() {
          this.dummyHead = new MyLinkedListMapNode();
          this.size = 0;
       }
    
       // 根据key获取节点 -
       getNode(key) {
          let cur = this.dummyHead.next;
    
          while (cur !== null) {
             if (cur.key === key) return cur;
             cur = cur.next;
          }
    
          return null;
       }
    
       // 添加操做 +
       add(key, value) {
          let node = this.getNode(key);
          // 这个节点若是存在就 覆盖值便可
          if (node !== null) node.value = value;
          else {
             // 若是不存在,那么就在头部添加如下
             let newNode = new MyLinkedListMapNode(key, value);
             newNode.next = this.dummyHead.next;
             this.dummyHead.next = newNode;
             this.size++;
          }
       }
    
       // 删除操做 返回被删除的元素 +
       remove(key) {
          let prev = this.dummyHead;
          // 循环查找
          while (prev.next !== null) {
             if (prev.next.key === key) break;
             prev = prev.next;
          }
    
          // 若是触碰了break, 那就知足条件
          if (prev.next !== null) {
             let delNode = prev.next;
             prev.next = delNode.next;
    
             let value = delNode.value;
             delNode = delNode.next = null;
             this.size--;
             return value;
          }
    
          // 若是没有触屏break 那就返回空值回去
          return null;
       }
    
       // 查询操做 返回查询到的元素 +
       get(key) {
          let node = this.getNode(key);
          if (node === null) return null;
          return node.value;
       }
    
       // 修改操做 +
       set(key, value) {
          let node = this.getNode(key);
          if (node === null) throw new Error(key + " doesn't exist.");
    
          node.value = value;
       }
    
       // 返回是否包含该key的元素的判断值 +
       contains(key) {
          return this.getNode(key) !== null;
       }
    
       // 返回映射中实际的元素个数 +
       getSize() {
          return this.size;
       }
    
       // 返回映射中是否为空的判断值 +
       isEmpty() {
          return this.size === 0;
       }
    
       // @Override toString() 2018-11-05-jwl
       toString() {
          let mapInfo = `MyLinkedListMap: size = ${this.size}, data = [ `;
          document.body.innerHTML += `MyLinkedListMap: size = ${ this.size }, data = [ <br/><br/>`;
    
          let cur = this.dummyHead.next;
    
          for (var i = 0; i < this.size - 1; i++) {
             mapInfo += ` ${cur.toString()}, \r\n`;
             document.body.innerHTML += ` ${cur.toString()}, <br/><br/>`;
             cur = cur.next;
          }
    
          if (cur !== null) {
             mapInfo += ` ${cur.toString()} \r\n`;
             document.body.innerHTML += ` ${cur.toString()} <br/><br/>`;
          }
    
          mapInfo += ` ] \r\n`;
          document.body.innerHTML += ` ] <br/><br/>`;
    
          return mapInfo;
       }
    }
    复制代码
  3. MyBSTMap

    // 自定义二分搜索树树映射节点 TreeMapNode
    class MyBinarySearchTreeMapNode {
       constructor(key = null, value = null, left = null, right = null) {
          this.key = key;
          this.value = value;
          this.left = left;
          this.right = right;
       }
    
       // @Override toString 2018-11-5-jwl
       toString() {
          return this.key.toString() + '---------->' + this.value.toString();
       }
    }
    
    // 自定义二分搜索树映射 Map
    class MyBinarySearchTreeMap {
       constructor() {
          this.root = null;
          this.size = 0;
       }
    
       // 比较的功能
       compare(keyA, keyB) {
          if (keyA === null || keyB === null)
             throw new Error("key is error. key can't compare.");
          if (keyA > keyB) return 1;
          else if (keyA < keyB) return -1;
          else return 0;
       }
    
       // 根据key获取节点 -
       getNode(node, key) {
          // 先解决最基本的问题
          if (node === null) return null;
    
          // 开始将复杂的问题 逐渐缩小规模
          // 从而求出小问题的解,最后构建出原问题的解
          switch (this.compare(node.key, key)) {
             case 1: // 向左找
                return this.getNode(node.left, key);
                break;
             case -1: // 向右找
                return this.getNode(node.right, key);
                break;
             case 0: // 找到了
                return node;
                break;
             default:
                throw new Error(
                   'compare result is error. compare result : 0、 一、 -1 .'
                );
                break;
          }
       }
    
       // 添加操做 +
       add(key, value) {
          this.root = this.recursiveAdd(this.root, key, value);
       }
    
       // 添加操做 递归算法 -
       recursiveAdd(node, key, value) {
          // 解决最简单的问题
          if (node === null) {
             this.size++;
             return new MyBinarySearchTreeMapNode(key, value);
          }
    
          // 将复杂的问题规模逐渐变小,
          // 从而求出小问题的解,从而构建出原问题的答案
          if (this.compare(node.key, key) > 0)
             node.left = this.recursiveAdd(node.left, key, value);
          else if (this.compare(node.key, key) < 0)
             node.right = this.recursiveAdd(node.right, key, value);
          else node.value = value;
    
          return node;
       }
    
       // 删除操做 返回被删除的元素 +
       remove(key) {
          let node = this.getNode(this.root, key);
          if (node === null) return null;
    
          this.root = this.recursiveRemove(this.root, key);
          return node.value;
       }
    
       // 删除操做 递归算法 +
       recursiveRemove(node, key) {
          // 解决最基本的问题
          if (node === null) return null;
    
          if (this.compare(node.key, key) > 0) {
             node.left = this.recursiveRemove(node.left, key);
             return node;
          } else if (this.compare(node.key, key) < 0) {
             node.right = this.recursiveRemove(node.right, key);
             return node;
          } else {
             // 当前节点的key 与 待删除的key的那个节点相同
             // 有三种状况
             // 1. 当前节点没有左子树,那么只有让当前节点的右子树直接覆盖当前节点,就表示当前节点被删除了
             // 2. 当前节点没有右子树,那么只有让当前节点的左子树直接覆盖当前节点,就表示当前节点被删除了
             // 3. 当前节点左右子树都有, 那么又分两种状况,使用前驱删除法或者后继删除法
             // 1. 前驱删除法:使用当前节点的左子树上最大的那个节点覆盖当前节点
             // 2. 后继删除法:使用当前节点的右子树上最小的那个节点覆盖当前节点
    
             if (node.left === null) {
                let rightNode = node.right;
                node.right = null;
                this.size--;
                return rightNode;
             } else if (node.right === null) {
                let leftNode = node.left;
                node.left = null;
                this.size--;
                return leftNode;
             } else {
                let predecessor = this.maximum(node.left);
                node.left = this.removeMax(node.left);
                this.size++;
    
                // 开始嫁接 当前节点的左右子树
                predecessor.left = node.left;
                predecessor.right = node.right;
    
                // 将当前节点从根节点剔除
                node = node.left = node.right = null;
                this.size--;
    
                // 返回嫁接后的新节点
                return predecessor;
             }
          }
       }
    
       // 删除操做的两个辅助函数
       // 获取最大值、删除最大值
       // 之前驱的方式 来辅助删除操做的函数
    
       // 获取最大值
       maximum(node) {
          // 不再能往右了,说明当前节点已是最大的了
          if (node.right === null) return node;
    
          // 将复杂的问题渐渐减少规模,从而求出小问题的解,最后用小问题的解构建出原问题的答案
          return this.maximum(node.right);
       }
    
       // 删除最大值
       removeMax(node) {
          // 解决最基本的问题
          if (node.right === null) {
             let leftNode = node.left;
             node.left = null;
             this.size--;
             return leftNode;
          }
    
          // 开始化归
          node.right = this.removeMax(node.right);
          return node;
       }
    
       // 查询操做 返回查询到的元素 +
       get(key) {
          let node = this.getNode(this.root, key);
          if (node === null) return null;
          return node.value;
       }
    
       // 修改操做 +
       set(key, value) {
          let node = this.getNode(this.root, key);
          if (node === null) throw new Error(key + " doesn't exist.");
    
          node.value = value;
       }
    
       // 返回是否包含该key的元素的判断值 +
       contains(key) {
          return this.getNode(this.root, key) !== null;
       }
    
       // 返回映射中实际的元素个数 +
       getSize() {
          return this.size;
       }
    
       // 返回映射中是否为空的判断值 +
       isEmpty() {
          return this.size === 0;
       }
    
       // @Override toString() 2018-11-05-jwl
       toString() {
          let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `;
          document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [ <br/><br/>`;
    
          // 以非递归的前序遍历 输出字符串
          let stack = new MyLinkedListStack();
    
          stack.push(this.root);
    
          if (this.root === null) stack.pop();
    
          while (!stack.isEmpty()) {
             let node = stack.pop();
    
             if (node.left !== null) stack.push(node.left);
             if (node.right !== null) stack.push(node.right);
    
             if (node.left === null && node.right === null) {
                mapInfo += ` ${node.toString()} \r\n`;
                document.body.innerHTML += ` ${node.toString()} <br/><br/>`;
             } else {
                mapInfo += ` ${node.toString()}, \r\n`;
                document.body.innerHTML += ` ${node.toString()}, <br/><br/>`;
             }
          }
    
          mapInfo += ` ] \r\n`;
          document.body.innerHTML += ` ] <br/><br/>`;
    
          return mapInfo;
       }
    }
    复制代码
  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);
       }
    
       // 计算运行的时间,转换为 天-小时-分钟-秒-毫秒
       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;
       }
    }
    复制代码
  5. Main

    // main 函数
    class Main {
       constructor() {
          this.alterLine('Map Comparison Area');
          let myLinkedListMap = new MyLinkedListMap();
          let myBinarySearchTreeMap = new MyBinarySearchTreeMap();
          let systemMap = new Map();
          let performanceTest = new PerformanceTest();
    
          systemMap.remove = systemMap.delete;
          systemMap.contains = systemMap.has;
          systemMap.add = systemMap.set;
          systemMap.isEmpty = () => systemMap.size === 0;
          systemMap.getSize = () => systemMap.size;
    
          let myLinkedListMapInfo = performanceTest.testMap(
             myLinkedListMap,
             50000
          );
          let myBinarySearchTreeMapInfo = performanceTest.testMap(
             myBinarySearchTreeMap,
             50000
          );
          let systemMapInfo = performanceTest.testMap(systemMap, 50000);
    
          this.alterLine('MyLinkedListMap Area');
          console.log(myLinkedListMapInfo);
          this.show(myLinkedListMapInfo);
    
          this.alterLine('MyBinarySearchTreeMap Area');
          console.log(myBinarySearchTreeMapInfo);
          this.show(myBinarySearchTreeMapInfo);
    
          this.alterLine('SystemMap Area');
          console.log(systemMapInfo);
          this.show(systemMapInfo);
       }
    
       // 将内容显示在页面上
       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();
    };
    复制代码

更多 Map 相关的问题

有序映射和无序映射

  1. 有序映射是指在 map 中的键是具备顺序性的
    1. 映射中这些 key 就充当了集合中相应的元素 e,
    2. 只不过在映射中每个 key 都有一个 value 的值而已,
    3. 有序映射一般都是基于搜索树来实现的,
    4. 由于搜索树具备这样额外的能力,
    5. 能够维持数据的有序性。
  2. 无序映射是指在 map 中键不具备顺序性的
    1. 链表实现的映射也是无序映射,
    2. 并且它很是的慢,
    3. 无序映射一般基于哈希表来实现的。

多重映射

  1. 普通映射的键是不可以重复的
    1. 可是在极个别的状况下,
    2. 有些应用场景可能但愿映射 map 中
    3. 能够存储具备重复键的相应的数据对,
    4. 在这种状况下就须要使用多重映射了。
  2. 多重映射中的键能够重复

集合和映射的关系

  1. MySet
    1. void add (e) : 不能添加剧复元素
    2. void remove (e)
    3. boolean conatains (e)
    4. int getSize ()
    5. boolean isEmpty ()
  2. MyMap
    1. void add(k, v)
    2. V remove(k)
    3. boolean contains(k)
    4. V get(k)
    5. void set(k, v)
    6. int getSize()
    7. boolean isEmpty()
  3. 实现这两种数据结构的时候既可使用链表也可使用二分搜索树
    1. 在实现的过程当中,这两种数据结构有不少相同之处,
    2. 对于映射来讲它自己也是一个集合,
    3. 只不过是一个键 key 这样的集合,
    4. 并且每个 key 还带着一个 value 而已,
    5. 它的本质和集合并无太大的区别,
    6. 只不过最开始实现的二分搜索树只可以存储一个元素,
    7. 因此在用二分搜索树实现 map 的时候不少方法须要从新写一遍,
    8. 可是它的实质和集合中的逻辑没有什么大的区别,
    9. 因此集合和映射之间是存在这样的联系的。
  4. 在不少系统类库中彻底能够基于集合 set 的实现去实现映射 map
    1. 或者基于映射 map 的实现来实现集合 set,
    2. 其实这个方法很是的简单,
    3. 例如你有了一个集合的底层实现,
    4. 在这种状况下再完成一个映射的只须要重定义集合中的元素是什么,
    5. 这个时候你只须要定义集合中的元素是键值对(key/value),
    6. 而且必定要特别的强调对于这种新的键值的数据对比较的时候,
    7. 是以键 key 的值来进行比较的而不是去比较 value 的值,
    8. 在这样的定义下,对于集合的定义全部操做都会适用于映射,
    9. 不过对于映射还须要添加新的操做,
    10. 因此更加常见的的方式是基于映射 map 的底层实现,
    11. 直接包装出集合 set 来,
    12. 当你有了一个映射的底层实现的时候,
    13. 直接将相应的映射的键值对(key/value)中的 value 赋值为空便可,
    14. 也就是只使用 key 而不使用 value,只考虑键 key 不考虑值 value,
    15. 这样一来整个 map 就是一个键 key 的集合,
    16. 只考虑键的时候,get 方法和 set 方法就没有意义了,
    17. 这样就至关于实现了一个映射以后在对这个映射进行包装,
    18. 就能够包装出集合这个数据结构了。
    19. 集合映射的核心逻辑实际上是一致的。
  5. 其实你能够直接对链表和二分搜索树直接设置 key 和 value
    1. 这种很常见的设计思路,
    2. 平衡二叉树、红黑树这样的树结构直接带有 key 和 value。

解决 leetcode 上的更多集合和映射问题

  1. leetcode 上349.两个数组的交集
    1. https://leetcode-cn.com/problems/intersection-of-two-arrays/
    2. 这个交集不保留重复元素,
    3. 使用 系统内置 Set 便可
  2. leetcode 上350.两个数组的交集 II
    1. https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/
    2. 这个交集保留重复元素
    3. 使用 系统内置 Map 便可。
  3. 其实和哈希表相关的大多数问题,
    1. 可使用 Set 和 Map 来解决
    2. 其实系统内置的 Set 和 Map 都是经过哈希表来实现的,再底层才会是红黑树,
    3. 使用基于哈希表实现的集合或者映射来解决和哈希表相关的大多数问题。
  4. 系统内置的 Set 和 Map 是先基于 hash 表的底层实现,
    1. 而后 hash 表是再基于平衡二叉树的底层实现,
    2. set 和 map 的结构是相同的,因此从用户使用的角度来看,
    3. 能够彻底无论它们的底层是怎么回事儿,
    4. 只须要知道它们能够实现这样的功能就行了,
    5. 相应的也应该知道它们背后不一样的底层实现的时间复杂度是怎样的,
    6. 在多大数状况下使用平衡二叉树实现的 Set 和 Map,
    7. 在时间上是彻底没有问题的,logn 这个复杂度也是很是很是快的。
  5. 能够尝试去使用 Set 和 Map 去实现 leetcode 上的哈希表标签的问题
    1. https://leetcode-cn.com/tag/hash-table/

代码示例

  1. (class: Solution, class: Solution)

  2. 两道题目

    1. Solution:leetcode 上349.两个数组的交集
    2. Solution:leetcode 上350.两个数组的交集 II
    // 答题
    class Solution {
       // leetcode 349. 两个数组的交集
       intersection(nums1, nums2) {
          /** * @param {number[]} nums1 * @param {number[]} nums2 * @return {number[]} */
          var intersection = function(nums1, nums2) {
             let set = new Set();
             let arr = [];
    
             for (const num of nums1) set.add(num);
    
             for (const num of nums2) {
                if (set.has(num)) {
                   arr.push(num);
                   set.delete(num);
                }
             }
    
             return arr;
          };
    
          return intersection(nums1, nums2);
       }
    
       // leetcode 350.两个数组的交集 II
       intersect(nums1, nums2) {
          /** * @param {number[]} nums1 * @param {number[]} nums2 * @return {number[]} */
          var intersect = function(nums1, nums2) {
             let map = new Map();
             let arr = [];
    
             for (const num of nums1) {
                if (map.has(num)) map.set(num, map.get(num) + 1);
                else map.set(num, 1);
             }
    
             for (const num of nums2) {
                if (map.has(num)) {
                   arr.push(num);
                   let result = map.get(num) - 1;
                   map.set(num, result);
    
                   if (result === 0) map.delete(num);
                }
             }
    
             return arr;
          };
    
          return intersect(nums1, nums2);
       }
    }
    // main 函数
    class Main {
       constructor() {
          this.alterLine('leetcode 349. 两个数组的交集');
          let s = new Solution();
          var nums1 = [1, 2, 2, 1],
             nums2 = [2, 2];
          var nums3 = [4, 9, 5],
             nums4 = [9, 4, 9, 8, 4];
    
          console.log('[' + s.intersection(nums1, nums2) + ']');
          console.log('[' + s.intersection(nums3, nums4) + ']');
          this.show('[' + s.intersection(nums1, nums2) + ']');
          this.show('[' + s.intersection(nums3, nums4) + ']');
    
          this.alterLine('leetcode 350. 两个数组的交集 II');
    
          console.log('[' + s.intersect(nums1, nums2) + ']');
          console.log('[' + s.intersect(nums3, nums4) + ']');
          this.show('[' + s.intersect(nums1, nums2) + ']');
          this.show('[' + s.intersect(nums3, nums4) + ']');
       }
    
       // 将内容显示在页面上
       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();
    };
    复制代码
相关文章
相关标签/搜索