【從蛋殼到滿天飛】JS 資料結構解析和演算法實現-二分搜尋樹(二)

哎喲迪奧發表於2019-03-20

思維導圖

前言

【從蛋殼到滿天飛】JS 資料結構解析和演算法實現,全部文章大概的內容如下: Arrays(陣列)、Stacks(棧)、Queues(佇列)、LinkedList(連結串列)、Recursion(遞迴思想)、BinarySearchTree(二分搜尋樹)、Set(集合)、Map(對映)、Heap(堆)、PriorityQueue(優先佇列)、SegmentTree(線段樹)、Trie(字典樹)、UnionFind(並查集)、AVLTree(AVL 平衡樹)、RedBlackTree(紅黑平衡樹)、HashTable(雜湊表)

原始碼有三個:ES6(單個單個的 class 型別的 js 檔案) | JS + HTML(一個 js 配合一個 html)| JAVA (一個一個的工程)

全部原始碼已上傳 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. 二分搜尋樹遍歷的非遞迴實現比遞迴實現複雜很多

    1. 因為你使用了一個輔助的資料結構才能完成這個過程,
    2. 使用了棧這種資料結構模擬了系統呼叫棧,
    3. 在演算法語意解讀上遠遠比遞迴實現的演算法語意解讀要難很多。
  6. 二分搜尋樹的中序遍歷和後序遍歷的非遞迴實現更復雜

    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. 二分搜尋樹也有它的侷限性。

相關文章