【從蛋殼到滿天飛】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();
    };
    複製代碼
相關文章
相關標籤/搜索