【從蛋殼到滿天飛】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 (一個一個的工程)node

所有源代碼已上傳 github,點擊我吧,光看文章可以掌握兩成,動手敲代碼、動腦思考、畫圖才能夠掌握八成。git

本文章適合 對數據結構想了解而且感興趣的人羣,文章風格一如既往如此,就以爲手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加源碼,時間跨度也算將近半年時間了,但願對想學習數據結構的人或者正在學習數據結構的人羣有幫助。github

二分搜索樹的遍歷-非遞歸寫法

  1. 前序遍歷的遞歸寫法面試

    1. 前序遍歷是最天然的一種遍歷方式,
    2. 同時也是最經常使用的一種遍歷方式,
    3. 若是沒有特殊狀況的話,
    4. 在大多數狀況下都會使用前序遍歷。
    5. 先訪問這個節點,
    6. 而後訪問這個節點的左子樹,
    7. 再訪問這個節點的右子樹,
    8. 整個過程循環往復。
    9. 前序遍歷的表示先訪問的這個節點。
    function preOrder(node) {
       if (node == null) return;
    
       // ... 要作的事情
       // 訪問該節點
    
       // 先一直往左,而後不斷返回上一層 再向左、終止,
       // 最後整個操做循環往復,直到所有終止。
       preOrder(node.left);
       preOrder(node.right);
    }
    複製代碼
  2. 前序遍歷的非遞歸寫法算法

    1. 使用另一個數據結構來模擬遞歸調用時的系統棧。
    2. 先訪問根節點,將根節點壓入棧,
    3. 而後把棧頂元素拿出來,對這個節點進行操做,
    4. 這個節點操做完畢以後,再訪問這個節點的兩個子樹,
    5. 也就是把這個節點的左右兩個孩子壓入棧中,
    6. 壓入棧的順序是先壓入右孩子、再壓入左孩子,
    7. 這是由於棧是後入先出的,因此要先壓入後續要訪問的那個節點,
    8. 再讓棧頂的元素出棧,對這個節點進行操做,
    9. 這個節點操做完畢以後,再訪問這個節點的兩個子樹,
    10. 可是這個節點是葉子節點,它的兩個孩子都爲空,
    11. 那麼什麼都不用壓入了, 再去取棧頂的元素,
    12. 對這個節點進行操做,這個節點操做完畢以後,
    13. 再訪問這個節點的兩個子樹,可是這個節點也是葉子節點,
    14. 那麼什麼都不用壓入了,棧中也爲空了,整個訪問操做結束。
  3. 不管是非遞歸仍是遞歸的寫法,結果都是一致的數組

    1. 非遞歸的寫法中,棧的應用是幫助你記錄你下面要訪問的哪些節點,
    2. 這個過程很是像使用棧模擬了一下在系統棧中相應的一個調用,
    3. 至關於在系統棧中記錄下一步依次要訪問哪些節點。
  4. 將遞歸算法轉換爲非遞歸算法數據結構

    1. 是棧這種數據結構很是重要的一種應用。
  5. 二分搜索樹遍歷的非遞歸實現比遞歸實現複雜不少dom

    1. 由於你使用了一個輔助的數據結構才能完成這個過程,
    2. 使用了棧這種數據結構模擬了系統調用棧,
    3. 在算法語意解讀上遠遠比遞歸實現的算法語意解讀要難不少。
  6. 二分搜索樹的中序遍歷和後序遍歷的非遞歸實現更復雜ide

    1. 尤爲是對於後序遍從來說難度更大,
    2. 可是中序遍歷和後序遍歷的非遞歸實現,實際應用並不普遍。
    3. 可是你能夠嘗試實現中序、後序遍歷的非遞歸實現,
    4. 主要是鍛鍊你算法實現、思惟邏輯實現思路,
    5. 在解決這個問題的過程當中可能會遇到一些困難,
    6. 能夠經過查看網上的資料來解決這個問題,
    7. 這樣的問題有可能會在面試題及考試中出現,
    8. 也就是中序和後序遍歷相應的非遞歸實現。
    9. 在經典的教科書中通常都會有這三種遍歷的非遞歸實現,
    10. 經過二分搜索樹的前序遍歷非遞歸的實現方式中能夠看出,
    11. 徹底可使用模擬系統的棧來完成遞歸轉成非遞歸這樣的操做,
    12. 在慕課上 有一門課《玩轉算法面試》中徹底模擬了系統棧的寫法,
    13. 也就是將前中後序的遍歷都轉成了非遞歸的算法,
    14. 這與經典的教科書上的實現不同,
    15. 可是這種方式對你進一步理解棧這種數據結構仍是二分搜索樹的遍歷
    16. 甚至是系統調用的過程都是頗有意義的。
  7. 對於前序遍從來說不管是遞歸寫法仍是非遞歸寫法

    1. 對於這棵樹來講都是在遍歷的過程當中一直到底,
    2. 這樣的一種遍歷方式也叫深度優先遍歷,
    3. 最終的遍歷結果都會先來到整顆樹最深的地方,
    4. 直到不能再深了纔會開始返回到上一層,
    5. 因此這種遍歷就叫作深度優先遍歷。
    6. 與深度優先遍歷相對應的就是廣度優先遍歷,
    7. 廣度優先遍歷遍歷出來的結果它的順序實際上是
    8. 整個二分搜索樹的一個層序遍歷的順序。

代碼示例(class: MyBinarySearchTree, class: Main)

  1. 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;
       }
    
       // 前序遍歷 +
       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);
       }
    
       // 獲取二分搜索樹中節點個數 +
       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;
       }
    }
    複製代碼
  2. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('MyBinarySearchTree Area');
          let myBinarySearchTree = new MyBinarySearchTree();
          let nums = [5, 3, 6, 8, 4, 2];
          for (var i = 0; i < nums.length; i++) {
             myBinarySearchTree.add(nums[i]);
          }
    
          /////////////////
          // 5 //
          // / \ //
          // 3 6 //
          // / \ \ //
          // 2 4 8 //
          /////////////////
    
          this.alterLine('MyBinarySearchTree PreOrder Area');
          myBinarySearchTree.preOrder(this.show);
    
          this.alterLine('MyBinarySearchTree NonRecursivePreOrder Area');
          myBinarySearchTree.nonRecursivePreOrder(this.show);
    
          this.alterLine('MyBinarySearchTree InOrder Area');
          myBinarySearchTree.inOrder(this.show);
    
          this.alterLine('MyBinarySearchTree PostOrder Area');
          myBinarySearchTree.postOrder(this.show);
       }
    
       // 將內容顯示在頁面上
       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. 對於二分搜索樹來講
    1. 每個節點都有一個相應的深度的值,
    2. 根節點做爲深度爲 0 相應的節點,
    3. 有一些教科書 會把根節點做爲深度爲 1 相應的節點,
    4. 若是以計算機世界裏索引的定義爲準那就是使用 0,
    5. 根節點就是第 0 層。
  3. 先遍歷第 0 層、再遍歷第 1 層、再遍歷下一層,
    1. 這樣的一層一層的遍歷就稱爲廣度優先遍歷,
    2. 逐層向下遍歷的節點在廣度上進行拓展,
    3. 這樣的一個遍歷順序就叫作層序遍歷、廣度優先遍歷,
    4. 而不像以前那樣 先順着一個枝杈向着最深的地方走。
  4. 對於層序遍歷的實現或者廣度優先遍歷的實現
    1. 一般不是使用遞歸的方式進行實現的,
    2. 而是使用非遞歸的方式進行實現的,
    3. 而且在其中須要使用另外的一個數據結構隊列,
    4. 從根節點開始排着隊的進入這個隊列,
    5. 隊列中存儲的就是待遍歷的元素,
    6. 每一次遍歷的它的元素以後再將它的左右孩子也排進隊列中,
    7. 整個過程依此類推。
  5. 先入隊根節點,而後看隊首是否有元素,
    1. 有的話就對隊首的元素進行操做,
    2. 操做完畢後就將操做完畢的元素的左右孩子也入隊,
    3. 而後再對隊列中的元素進行操做,
    4. 隊列中的元素又操做完畢了,
    5. 再讓操做完畢的這些元素的左右孩子入隊,
    6. 最後在對隊列中的元素進行操做,
    7. 這些元素都是葉子節點沒有左右孩子了,,
    8. 不用入隊了,隊列中沒有元素,整個過程處理完畢,
    9. 這個處理過程就是一層一層的進行處理的一個順序,
    10. 這就是二分搜索樹的廣度優先遍歷,也叫層序遍歷。
  6. 相對於深度優先遍從來說,廣度優先遍歷的優勢
    1. 它能更快的找到你想要查詢的那個元素,
    2. 這樣的區別主要用於搜索策略上,
    3. 而不是用在遍歷這個操做上,
    4. 雖然遍歷要將整個二叉樹上全部的元素都訪問一遍,
    5. 這種狀況下深度優先遍歷和廣度優先遍歷是沒有區別的。
    6. 可是若是想在一棵樹中找到某一個問題的解,
    7. 那對於深度優先遍從來說
    8. 它會從根節點一股腦的跑到這棵樹很是深的地方,
    9. 可是頗有可能這個問題的解並不在那麼深的地方而是很淺的地方,
    10. 這樣一來深度優先遍歷要花很長時間才能訪問到這個很淺的地方,
    11. 例如前序遍歷,若是這個問題的解在右子樹上很淺的位置,
    12. 你從一開始就從根節點遍歷到左子樹的最深處,那就不必了,
    13. 可是這個經常使用於算法設計中,如無權圖的最短路徑,
    14. 樹這種結構在算法設計裏也有很是重要的應用,
    15. 尤爲是不少時候設計出一個算法,可能真正不須要把這個樹發現出來,
    16. 可是這個算法的整個過程就是在一棵虛擬的樹中完成的。
  7. 在圖中也是有深度優先遍歷和廣度優先遍歷的
    1. 在樹中和圖中進行深度優先遍歷其實它們的實質是同樣的,
    2. 不一樣的點,對於圖來講須要記錄一下對於某一個節點以前是否曾經遍歷過,
    3. 由於對於圖來講每個節點的前驅或者放在樹這個模型中
    4. 相應的術語就是每一節點它的父親可能有多個,
    5. 從而產生重複訪問這樣的問題,而這樣的問題在樹結構中是不存在的,
    6. 因此在圖結構中須要作一個相應的記錄。

代碼示例(class: MyBinarySearchTree, class: Main)

  1. 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;
       }
    
       // 前序遍歷 +
       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;
       }
    }
    複製代碼
  2. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('MyBinarySearchTree Area');
          let myBinarySearchTree = new MyBinarySearchTree();
          let nums = [5, 3, 6, 8, 4, 2];
          for (var i = 0; i < nums.length; i++) {
             myBinarySearchTree.add(nums[i]);
          }
    
          /////////////////
          // 5 //
          // / \ //
          // 3 6 //
          // / \ \ //
          // 2 4 8 //
          /////////////////
    
          this.alterLine('MyBinarySearchTree PreOrder Area');
          myBinarySearchTree.preOrder(this.show);
    
          this.alterLine('MyBinarySearchTree NonRecursivePreOrder Area');
          myBinarySearchTree.nonRecursivePreOrder(this.show);
    
          this.alterLine('MyBinarySearchTree InOrder Area');
          myBinarySearchTree.inOrder(this.show);
    
          this.alterLine('MyBinarySearchTree PostOrder Area');
          myBinarySearchTree.postOrder(this.show);
    
          this.alterLine('MyBinarySearchTree LevelOrder Area');
          myBinarySearchTree.levelOrder(this.show);
       }
    
       // 將內容顯示在頁面上
       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. 以及本身去總結不一樣的方法適用於什麼樣的場合,
    5. 只有這樣,這些知識才可以在你的腦海中才不是一個一個的碎片,
    6. 而是有機的聯繫起來的,面對不一樣的問題才能很是的快的
    7. 而且準確的說出來用怎樣的方法去解決更加的好。

二分搜索樹的刪除節點-刪除最大最小值

  1. 對於二分搜索樹來講刪除一個節點相對來講是比較複雜的
    1. 能夠先對這個操做進行拆解,從最簡單的開始。
  2. 刪除二分搜索樹的最小值和最大值
    1. 刪除二分搜索樹中任意元素會複用到
    2. 刪除二分搜索樹最大值和最小值相應的邏輯。
    3. 要想刪除二分搜索樹中最大值和最小值,
    4. 那麼就要先找到二分搜索樹中的最大值和最小值。
  3. 找到二分搜索樹中的最大值和最小值是很是容易的
    1. 每個節點的左子樹上全部的節點的值都小於當前這個節點,
    2. 每個節點的右子樹上全部的節點的值都大於當前這個節點,
    3. 那麼從根節點開始一直向左,直到不能再左了,就能找到最小值,
    4. 反之從根節點開始一直向右,知道不能再右了,就能找到最大值。
    5. 這個操做就像操做鏈表同樣,就像是在找一條鏈上的尾節點。
  4. 刪除最大元素節點
    1. 要刪除最大元素的這個節點可能有左孩子節點可是沒有右孩子節點,
    2. 因此可能會致使沒法繼續向右因而遞歸就終止了,
    3. 那麼這個時候刪除這個節點能夠採用當前節點的左孩子替代當前這個節點,
    4. 覆蓋操做也算是刪除了當前這個節點了。
    5. 若是你像返回被刪除的這個最大元素節點,你能夠先查詢出這個最大的元素節點,
    6. 而後存到一個變量中,最後再調用刪除這個最大元素節點的方法,最終返回存的這個變量。
  5. 刪除最小元素節點
    1. 要刪除的最小元素的節點可能有右孩子節點可是沒有左孩子節點,
    2. 會致使沒法繼續向左而遞歸終止,你不能刪除這個節點的同時連右孩子一塊兒刪除,
    3. 因此這個時候刪除這個節點能夠採用當前節點的右孩子替代當前這個節點,
    4. 覆蓋操做也算是刪除了當前這個節點了,
    5. 其它的和刪除最大元素同樣,先查詢出來,而後存起來,刪除這個最大元素後,
    6. 再返回以前存起來的最大元素的變量。

代碼示例(class: MyBinarySearchTree, class: Main)

  1. 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;
       }
    
       // 前序遍歷 +
       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;
       }
    }
    複製代碼
  2. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('MyBinarySearchTree remove Min Node Area');
          {
             let tree = new MyBinarySearchTree();
    
             let n = 100;
             let random = Math.random;
    
             for (var i = 0; i < n; i++) {
                tree.add(n * n * n * random());
             }
    
             let array = new MyArray(n);
    
             while (!tree.isEmpty()) {
                array.add(tree.removeMin());
             }
    
             // 數組中的元素從小到大排序的
             console.log(array.toString());
    
             for (var i = 1; i < n; i++) {
                //若是數組後面的元素小於數組前面的元素
                if (array.get(i) < array.get(i - 1))
                   throw new Error(
                      'error. array element is not (small - big) sort.'
                   );
             }
    
             console.log('removeMin test completed.');
             this.show('removeMin test completed.');
          }
    
          this.alterLine('MyBinarySearchTree remove Max Node Area');
          {
             let tree = new MyBinarySearchTree();
    
             let n = 100;
             let random = Math.random;
    
             for (var i = 0; i < n; i++) {
                tree.add(n * n * n * random());
             }
    
             let array = new MyArray(n);
    
             while (!tree.isEmpty()) {
                array.add(tree.removeMax());
             }
    
             // 數組中的元素從大到小排序的
             console.log(array.toString());
    
             for (var i = 1; i < n; i++) {
                //若是數組後面的元素大於數組前面的元素
                if (array.get(i) > array.get(i - 1))
                   throw new Error(
                      'error. array element is not (big - small) sort.'
                   );
             }
    
             console.log('removeMax test completed.');
             this.show('removeMax test completed.');
          }
       }
    
       // 將內容顯示在頁面上
       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. 而後返回,而後讓這條分支上每一個節點的左或者右子樹進行層層覆蓋,
    5. 而後層層返回新的節點,直到最後返回給根節點、覆蓋掉根節點,
    6. 從而達到了刪除最小或最大節點的目的。
    7. 刪除最小值的節點就不停的向左遍歷,最後記錄右子樹,
    8. 由於被刪除的節點要被這個節點的右子樹替代掉,
    9. 只有這樣纔可以達到刪除最小值的節點的效果。
    10. 刪除最大值的節點就不停的向右遍歷,最後記錄左子樹,
    11. 由於被刪除的節點要被這個節點的左子樹替代掉,
    12. 只有這樣纔可以達到刪除最大值的節點的效果。
  2. 刪除二分搜索樹上任意節點會發生的狀況
    1. 刪除的這個節點只有左孩子,這個邏輯和上面的相似,
    2. 就讓這個節點的左孩子取代這個節點的位置。
    3. 刪除的這個節點只有右孩子,這個邏輯也是同樣,
    4. 就讓這個節點的右孩子取代這個節點的位置。
    5. 刪除的這個節點是葉子節點,這個邏輯也同樣,
    6. 由於 null 也是一個二分搜索樹、也是一個節點、也是一個孩子,
    7. 直接讓 null 取代這個節點的位置便可。
    8. 真正難的地方是去刪除左右都有孩子這樣的節點,
    9. 在 1962 年,Hibbard(計算機科學家)提出-Hibbard Deletion,
    10. 找到離這個節點的值最近而且大的那個節點來取代這個節點,
    11. 也就是找到 這個節點的右孩子的左孩子(右子樹的左子樹上最小的節點),
    12. 例如待刪除的節點爲 d,那麼就是 s = min(d->right),
    13. 找到比當前節點大最小且最近的節點,這個 s 就是 d 的後繼,
    14. 執行 s->right = delMin(d->right)這樣的操做,
    15. 以後讓 s->left = d->left,
    16. 刪除的 d 後,s 是新的子樹的根,返回這個 s 節點就能夠了。
    17. 除了找待刪除節點 d 的後繼 s 以外,還能夠找待刪除節點的前驅 p,
    18. 也就是找到 這個節點的左孩子的右孩子(左子樹的右子樹上最大的節點)。
    19. 不管使用前驅仍是後繼來取代待刪除的這個節點
    20. 都可以繼續保持二分搜索樹的性質。
  3. 對於二分搜索樹來講
    1. 相對於數組、棧、隊列、鏈表這些數據結構要複雜一些,
    2. 二分搜索樹自己也是學習其它的樹,如 平衡二叉樹的基礎。

代碼示例(class: MyBinarySearchTree, class: Main)

  1. 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;
       }
    }
    複製代碼
  2. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('MyBinarySearchTree Remove Node Area');
          {
             let n = 100;
    
             let tree = new MyBinarySearchTree();
             let array = new MyArray(n);
    
             let random = Math.random;
    
             for (var i = 0; i < n; i++) {
                tree.add(n * n * n * random());
                array.add(tree.removeMin());
             }
    
             // 數組中的元素從小到大排序的
             console.log(array.toString());
    
             for (var i = 0; i < n; i++) {
                tree.remove(array.get(i));
             }
    
             console.log(
                'removeMin test ' +
                   (tree.isEmpty() ? 'completed.' : 'no completed.')
             );
             this.show(
                'removeMin test ' +
                   (tree.isEmpty() ? 'completed.' : 'no completed.')
             );
          }
       }
    
       // 將內容顯示在頁面上
       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. 添加元素 add
  2. 刪除元素 remove
  3. 查詢元素 contains
  4. 遍歷元素 order

其它實現的二分搜索樹功能

  1. 能夠很是方便的拿到二分搜索樹中最大值和最小值, 2. 這是由於二分搜索樹自己有一個很是重要的特性, 3. 也就是二分搜索樹具備順序性, 4. 這個順序性就是指 二分搜索樹中全部的元素都是有序的, 5. 例如使用中序遍歷遍歷的元素就是將元素從小到大排列起來, 6. 也正是有順序性纔可以很方便的得到 7. 二分搜索樹中最大值(maximum)最小值(minimum), 8. 包括給定一個值能夠拿到它的前驅(predecessor)和後繼(successor)。
  2. 也由於這個順序性也能夠對它進行 floor 和 ceil 的操做,
    1. 也就是找比某一個元素值大的元素或者值小的元素,
    2. 前驅、後繼中指定的元素必定要在這棵二分搜索樹中,
    3. 而 floor 和 ceil 中指定的這個元素能夠不在這棵二分搜索樹中。
  3. 相應的二分搜索樹還能夠實現 rank 和 select 方法,
    1. rank 也就是指定一個元素找到它的排名,
    2. select 是一個反向的操做,也就是找到排名爲多少名的那個元素。
    3. 對於二分搜索樹來講均可以很是容易的實現這兩個操做。
    4. 實現 rank 和 select 最好的方式是對於二分搜索樹每個節點
    5. 同時還維護一個 size,
    6. 這個 size 就是指以這個節點爲根的二分搜索樹有多少個元素,
    7. 也就是每個節點爲根的二分搜索樹中有多少的元素,
    8. 那麼這個 size 就爲多少,
    9. 也就是每個節點包括本身以及下面的子節點的個數,
    10. 每個 node 在維護了一個 size 以後,
    11. 那麼實現 rank 和 select 這兩個操做就會容易不少,
    12. 也就是給 node 這個成員變量添加一個 size,
    13. 那麼對於二分搜索樹其它操做如添加和刪除操做時,
    14. 也要去維護一下這個節點的 size,
    15. 只有這樣實現這個 rank 和 select 就會很是簡單,
    16. 這樣作以後,對於整棵二分搜索樹而言,
    17. 就再也不須要二分搜索樹的 size 變量了,
    18. 若是要看整棵二分搜索樹有多少個節點,
    19. 直接看root.size就行了,很是的方便。
  4. 維護 depth 的二分搜索樹
    1. 對於二分搜索樹的每個節點還能夠維護一個深度值,
    2. 也就是這個節點的高度值,也就是這個節點處在第幾層的位置,
    3. 維護這個值在一些狀況下是很是有幫助的。
  5. 支持重複元素的二分搜索樹
    1. 只須要定義每個根節點的左子樹全部的節點都是
    2. 小於等於這個根節點值的,
    3. 而每個根節點的右子樹全部的節點都是大於這個根節點值的,
    4. 這樣的定義就很好的支持了重複元素的二叉樹的實現。
  6. 還能夠經過維護每個節點的 count 變量來實現重複元素的二分搜索樹,
    1. 也就是記錄一下這個節點所表明的元素在這個二分搜索樹中存儲的個數,
    2. 當你添加進重複的節點後,直接讓相應節點的count++便可,
    3. 若是你刪除這個重複的節點時,直接讓相應節點的count--便可,
    4. 若是 count 減減以後爲 0,那麼就從二分搜索樹中真正刪除掉。

其它

  1. 在二分搜索樹中相應的變種其實大可能是在 node 中維護一些數據
    1. 就能夠方便你進行一些其它特殊狀況的處理,
  2. 相關的習題能夠去 leetcode 中找到,
    1. 樹標籤:https://leetcode-cn.com/tag/tree/
    2. 如第一題,二叉樹的最大深度,這個題和鏈表是很是像的,
    3. 它有一個答題的模板,你提交的時候要按照這個模板來進行提交。
  3. 其它
    1. 二分搜索樹的複雜度分析,
    2. 二分搜索樹有兩個重要的應用集合和映射,
    3. 其實用數組和鏈表也可以實現集合和映射,
    4. 二分搜索樹也有它的侷限性。
相關文章
相關標籤/搜索