【從蛋殼到滿天飛】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. 那麼就須要進行左旋轉的操做,不然就會違背紅黑樹的性質。
  2. 在紅黑樹中的三節點中添加元素的第一種狀況算法

    1. 下圖中節點 37 和節點 42 已經造成了一個三節點,數組

    2. 由於所謂紅色節點的意思就是它和它的父親是融合在一塊兒的,數據結構

    3. 若是這個時候再添加一個元素 66,新添加的節點默認爲紅色,dom

    4. 那麼按照二分搜索樹中的性質,這個元素會被添加爲根節點 44 的右孩子,ide

    5. 此時就會對應二三樹中臨時的四節點,在紅黑樹的表示就是,

    6. 根節點是黑色的,左右孩子節點都是紅色的,

    7. 所謂紅色節點的意思就是它和它的父親是融合在一塊兒的。

      // 中括號爲黑色節點,大括號爲紅色節點
      // [42]
      // /
      // {37}
      
      // 添加元素 66
      // [42]
      // / \
      // {37} {66}
      複製代碼
  3. 顏色翻轉

    1. 在二三樹中這個臨時的四節點以後會被分裂成三個二節點,

    2. 對應紅黑樹中的操做是將根節點 42 下面兩個節點 37 和 66 都染成黑色,

    3. 這樣就表示了在二三樹中三個二節點組成了一棵子樹,

    4. 臨時的四節點分裂以後,新的子樹的根節點須要向上進行融合操做,

    5. 因此在紅黑樹中的操做是,這個根節點 42 要變成紅色,

    6. 由於所謂紅色節點的意思就是它和它的父親是融合在一塊兒的,

    7. 這個過程就讓根節點的顏色與左右子樹的顏色進行了一個翻轉操做,

    8. 顏色翻轉的英文就是 flipColors,這是在紅黑樹中添加一個新元素的輔助過程,

    9. 實際上也是等價的在二三樹中的三節點中添加了一個新元素所對應的一個輔助過程。

      // 中括號爲黑色節點,大括號爲紅色節點
      // 添加元素 66
      // [42]
      // / \
      // {37} {66}
      
      // 顏色翻轉
      // {42}
      // / \
      // [37] [66]
      複製代碼
  4. 在紅黑樹中的三節點中添加元素的另一種狀況

    1. 下圖中節點 37 和節點 42 已經造成了一個三節點,,
    2. 若是這個時候再添加一個元素 12,新添加的節點默認爲紅色,
    3. 那麼按照二分搜索樹中的性質,這個元素會被添加爲根節點 44 的左孩子 37 的左孩子,
    4. 這樣一來就至關於節點 42 的左孩子是紅色節點而後左孩子的左孩子仍是紅色節點,
    5. 所謂紅色的節點,就是它與它的父節點是融合在了一塊兒,
    6. 這樣的一種狀況也能夠將它理解爲一個臨時的四節點,
    7. 節點 23 是紅色的,它和它的父節點 37 融合在了一塊兒,而後節點 37 是紅色,
    8. 它和它的父節點 42 也是融合在一塊兒的,因此這種形狀也表示對應二三樹中的四節點,
    9. 也是同樣的,須要對四節點進行分裂操做,變成一個由三個二節點組成的子樹,
    10. 因此在紅黑樹中須要將這樣的向左偏斜的形狀進行一個糾正,
    11. 要讓它變成一個根節點左右兩個孩子的形狀,此時就須要引入另一個子過程右旋轉。
    // 中括號爲黑色節點,大括號爲紅色節點
    // [42]
    // /
    // {37}
    
    // 添加元素 12
    // [42]
    // /
    // {37}
    // /
    // {23}
    複製代碼
  5. 右旋轉

    1. 讓節點 node 的左子樹與節點 x 進行斷開鏈接,讓節點 x 與右子樹 T1 斷開鏈接,
    2. 讓節點 x 的右子樹與節點 node 進行鏈接,讓節點 node 的左子樹與 T1 進行鏈接,
    3. 這樣就有一個右旋轉的過程,這個和左旋轉是相似的,
    4. 以後對於紅黑樹來講還須要維護一下顏色的信息,
    5. 這個維護的過程和以前講的左旋轉的過程是同樣的,
    6. 新的根節點 x 的顏色應該和原來的根節點 node 的顏色是同樣的,
    7. 通過這樣的處理以後,node 雖然變成了 x 的右孩子了,
    8. 可是須要將它染色成紅色,這是爲了保證後續的操做可以有效進行,
    9. 右旋轉的做用只是讓這三個節點對應成二三樹中的臨時四節點。
    10. 也仍是同樣,須要進行顏色翻轉操做,從而維護紅黑樹的性質,
    11. 新的子樹的根節點須要向上進行融合,因此它須要變成紅色,
    12. 在紅黑樹中紅色節點的左右子樹的顏色都是黑色的,
    13. 因此根節點的左右孩子節點顏色都要變成黑色。
    // 中括號爲黑色節點,大括號爲紅色節點
    // 小括號只是參與演示,並不真實存在
    // 原來是這樣的
    // [42] node
    // / \
    // x {37} (T2)
    // / \
    // {23} (T1)
    
    // 進行右旋轉後
    // {37} x
    // / \
    // {23} {42} node
    // / \
    // (T1) (T2)
    
    // node.left = T1
    // x.right = node;
    // x.color = node.color;
    // node.color = RED;
    
    // 顏色翻轉後
    // {37} x
    // / \
    // [23] [42] node
    // / \
    // (T1) (T2)
    
    // x.color = RED;
    // x.left.color = BLACK;
    // x.right.color = BLACK;
    複製代碼

代碼示例

  1. MyRedBlackTree

    // 自定義紅黑樹節點 RedBalckTreeNode
    class MyRedBalckTreeNode {
       constructor(key = null, value = null, left = null, right = null) {
          this.key = key;
          this.value = value;
          this.left = left;
          this.right = right;
          this.color = MyRedBlackTree.RED; // MyRedBlackTree.BLACK;
       }
    
       // @Override toString 2018-11-25-jwl
       toString() {
          return (
             this.key.toString() +
             '--->' +
             this.value.toString() +
             '--->' +
             (this.color ? '紅色節點' : '綠色節點')
          );
       }
    }
    
    // 自定義紅黑樹 RedBlackTree
    class MyRedBlackTree {
       constructor() {
          MyRedBlackTree.RED = true;
          MyRedBlackTree.BLACK = false;
    
          this.root = null;
          this.size = 0;
       }
    
       // 判斷節點node的顏色
       isRed(node) {
          // 定義:空節點顏色爲黑色
          if (!node) return MyRedBlackTree.BLACK;
    
          return node.color;
       }
    
       // node x
       // / \ 左旋轉 / \
       // T1 x ---------> node T3
       // / \ / \
       // T2 T3 T1 T2
       leftRotate(node) {
          const x = node.right;
    
          // 左旋轉過程
          node.right = x.left;
          x.left = node;
    
          // 染色過程
          x.color = node.color;
          node.color = MyRedBlackTree.RED;
    
          // 返回這個 x
          return x;
       }
    
       // 顏色翻轉 當前節點變紅 左右孩子變黑
       // 表示當前節點須要繼續向上進行融合
       flipColors(node) {
          node.color = MyRedBlackTree.RED;
          node.left.color = MyRedBlackTree.BLACK;
          node.right.color = MyRedBlackTree.BLACK;
       }
    
       // node x
       // / \ 右旋轉 / \
       // x T2 -------> y node
       // / \ / \
       // y T1 T1 T2
       rightRotate(node) {
          const x = node.left;
    
          // 右翻轉過程
          node.left = x.right;
          x.right = node;
    
          // 染色過程
          x.color = node.color;
          node.color = MyRedBlackTree.RED;
    
          // 返回這個 x
          return x;
       }
    
       // 比較的功能
       compare(keyA, keyB) {
          if (keyA === null || keyB === null)
             throw new Error("key is error. key can't compare.");
          if (keyA > keyB) return 1;
          else if (keyA < keyB) return -1;
          else return 0;
       }
    
       // 根據key獲取節點 -
       getNode(node, key) {
          // 先解決最基本的問題
          if (node === null) return null;
    
          // 開始將複雜的問題 逐漸縮小規模
          // 從而求出小問題的解,最後構建出原問題的解
          switch (this.compare(node.key, key)) {
             case 1: // 向左找
                return this.getNode(node.left, key);
                break;
             case -1: // 向右找
                return this.getNode(node.right, key);
                break;
             case 0: // 找到了
                return node;
                break;
             default:
                throw new Error(
                   'compare result is error. compare result : 0、 一、 -1 .'
                );
                break;
          }
       }
    
       // 添加操做 +
       add(key, value) {
          this.root = this.recursiveAdd(this.root, key, value);
          this.root.color = MyRedBlackTree.BLACK;
       }
    
       // 添加操做 遞歸算法 -
       recursiveAdd(node, key, value) {
          // 解決最簡單的問題
          if (node === null) {
             this.size++;
             return new MyRedBalckTreeNode(key, value);
          }
    
          // 將複雜的問題規模逐漸變小,
          // 從而求出小問題的解,從而構建出原問題的答案
          if (this.compare(node.key, key) > 0)
             node.left = this.recursiveAdd(node.left, key, value);
          else if (this.compare(node.key, key) < 0)
             node.right = this.recursiveAdd(node.right, key, value);
          else node.value = value;
    
          return node;
       }
    
       // 刪除操做 返回被刪除的元素 +
       remove(key) {
          let node = this.getNode(this.root, key);
          if (node === null) return null;
    
          this.root = this.recursiveRemove(this.root, key);
          return node.value;
       }
    
       // 刪除操做 遞歸算法 +
       recursiveRemove(node, key) {
          // 解決最基本的問題
          if (node === null) return null;
    
          if (this.compare(node.key, key) > 0) {
             node.left = this.recursiveRemove(node.left, key);
             return node;
          } else if (this.compare(node.key, key) < 0) {
             node.right = this.recursiveRemove(node.right, key);
             return node;
          } else {
             // 當前節點的key 與 待刪除的key的那個節點相同
             // 有三種狀況
             // 1. 當前節點沒有左子樹,那麼只有讓當前節點的右子樹直接覆蓋當前節點,就表示當前節點被刪除了
             // 2. 當前節點沒有右子樹,那麼只有讓當前節點的左子樹直接覆蓋當前節點,就表示當前節點被刪除了
             // 3. 當前節點左右子樹都有, 那麼又分兩種狀況,使用前驅刪除法或者後繼刪除法
             // 1. 前驅刪除法:使用當前節點的左子樹上最大的那個節點覆蓋當前節點
             // 2. 後繼刪除法:使用當前節點的右子樹上最小的那個節點覆蓋當前節點
    
             if (node.left === null) {
                let rightNode = node.right;
                node.right = null;
                this.size--;
                return rightNode;
             } else if (node.right === null) {
                let leftNode = node.left;
                node.left = null;
                this.size--;
                return leftNode;
             } else {
                let predecessor = this.maximum(node.left);
                node.left = this.removeMax(node.left);
                this.size++;
    
                // 開始嫁接 當前節點的左右子樹
                predecessor.left = node.left;
                predecessor.right = node.right;
    
                // 將當前節點從根節點剔除
                node = node.left = node.right = null;
                this.size--;
    
                // 返回嫁接後的新節點
                return predecessor;
             }
          }
       }
    
       // 刪除操做的兩個輔助函數
       // 獲取最大值、刪除最大值
       // 之前驅的方式 來輔助刪除操做的函數
    
       // 獲取最大值
       maximum(node) {
          // 不再能往右了,說明當前節點已是最大的了
          if (node.right === null) return node;
    
          // 將複雜的問題漸漸減少規模,從而求出小問題的解,最後用小問題的解構建出原問題的答案
          return this.maximum(node.right);
       }
    
       // 刪除最大值
       removeMax(node) {
          // 解決最基本的問題
          if (node.right === null) {
             let leftNode = node.left;
             node.left = null;
             this.size--;
             return leftNode;
          }
    
          // 開始化歸
          node.right = this.removeMax(node.right);
          return node;
       }
    
       // 查詢操做 返回查詢到的元素 +
       get(key) {
          let node = this.getNode(this.root, key);
          if (node === null) return null;
          return node.value;
       }
    
       // 修改操做 +
       set(key, value) {
          let node = this.getNode(this.root, key);
          if (node === null) throw new Error(key + " doesn't exist.");
    
          node.value = value;
       }
    
       // 返回是否包含該key的元素的判斷值 +
       contains(key) {
          return this.getNode(this.root, key) !== null;
       }
    
       // 返回映射中實際的元素個數 +
       getSize() {
          return this.size;
       }
    
       // 返回映射中是否爲空的判斷值 +
       isEmpty() {
          return this.size === 0;
       }
    
       // @Override toString() 2018-11-05-jwl
       toString() {
          let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `;
          document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [ <br/><br/>`;
    
          // 以非遞歸的前序遍歷 輸出字符串
          let stack = new MyLinkedListStack();
    
          stack.push(this.root);
    
          if (this.root === null) stack.pop();
    
          while (!stack.isEmpty()) {
             let node = stack.pop();
    
             if (node.left !== null) stack.push(node.left);
             if (node.right !== null) stack.push(node.right);
    
             if (node.left === null && node.right === null) {
                mapInfo += ` ${node.toString()} \r\n`;
                document.body.innerHTML += ` ${node.toString()} <br/><br/>`;
             } else {
                mapInfo += ` ${node.toString()}, \r\n`;
                document.body.innerHTML += ` ${node.toString()}, <br/><br/>`;
             }
          }
    
          mapInfo += ` ] \r\n`;
          document.body.innerHTML += ` ] <br/><br/>`;
    
          return mapInfo;
       }
    }
    複製代碼

向紅黑樹中添加新節點

  1. 在紅黑樹中添加新節點等價於在二三樹的一個二或三節點上融合一個新的元素

    1. 在二節點上進行融合操做,

    2. 若是新添加的這個節點比最後找到的葉子節點的元素小,

    3. 那麼對應在紅黑樹中的操做就是直接添加爲當前節點的左孩子便可;

    4. 若是新添加的這個節點比最後找到的葉子節點的元素大,

    5. 那麼對應在紅黑樹中的操做是 首先直接添加爲當前節點的右孩子,

    6. 而後再進行左旋轉操做,讓新添加的節點做爲根節點,

    7. 而原來的根節點做爲新添加的節點的左孩子節點便可;

    8. 左旋轉操做須要將新的根節點與原來的根節點的顏色對調,

    9. 而後將原來的根節點染色爲紅色,最後,

    10. 左旋轉的過程當中並不會去維護紅黑樹的性質,

    11. 左旋轉的做用只是讓這兩個節點對應成二三樹中的三節點。

    12. 在三節點上進行融合操做,

    13. 若是新添加的這個節點比根節點要大,

    14. 那麼對應在紅黑樹中的操做就是直接添加爲根節點的右孩子,

    15. 以後進行一下顏色的翻轉操做便可;

    16. 若是新添加的這個節點比根節點要小而且比根節點的左孩子要小,

    17. 那麼對應在紅黑樹中的操做就是先直接添加爲根節點的左孩子的左孩子,

    18. 而後進行一下右旋轉,

    19. 旋轉右旋轉操做須要將新的根節點與原來的根節點的顏色對調,

    20. 而後將原來的根節點染色爲紅色,

    21. 也是同樣,旋轉操做不會去維護紅黑樹的性質,

    22. 右旋轉的做用只是讓這三個節點對應成二三樹中的臨時四節點,

    23. 以後進行一下顏色的翻轉操做便可;

    24. 若是新添加的這個節點比根節點要小而且比根節點的左孩子要大,

    25. 那麼對應在紅黑樹中的操做就是先直接添加爲根節點的左孩子的右孩子,

    26. 而後對這個根節點的左孩子進行一個左旋轉操做,

    27. 左旋轉操做以後就以下圖所示,

    28. 就變成了新添加的這個節點比根節點要小而且比根節點的左孩子要小的狀況,

    29. 那麼就對這個根節點進行一個右旋轉操做,

    30. 再來一個顏色翻轉操做便可。

      // 中括號爲黑色節點,大括號爲紅色節點
      // 原來是這樣的
      // [42]
      // /
      // {37}
      // \
      // {40}
      
      // 對根節點的左孩子 進行左旋轉後 染色後
      // [42]
      // /
      // {40}
      // /
      // {37}
      
      // 對根節點 進行右旋轉後 染色後
      // [40]
      // / \
      // {37} {42}
      //
      
      // 對根節點 顏色翻轉後
      // {40}
      // / \
      // [37] [42]
      複製代碼
  2. 紅黑樹添加節點邏輯分佈圖

    1. 下圖中的邏輯只要分爲三種

    2. 是否須要左旋轉、右旋轉、顏色翻轉,

    3. 噹噹前節點的右孩子爲紅色而且當前節點的左孩子不爲紅色時,那麼就須要進行左旋轉;

    4. 噹噹前節點的左孩子爲紅色而且當前節點的左孩子的左孩子也爲紅色時,那麼就須要進行右旋轉;

    5. 噹噹前節點的左右孩子全都是紅色時,那麼就須要進行顏色翻轉;

    6. 這三種邏輯並不是是互斥的,而是相吸,每一次都須要進行這三種判斷,

    7. 下圖也是按照這樣的順序來進行步驟的執行的。

      // 中括號爲黑色節點,大括號爲紅色節點
      
      // 融合二節點
      // 步驟一 步驟二 步驟三
      // [null] [node] [node] [X]
      // ----> ----> \ ----> /
      // {X} {node}
      // 首次添加節點 ----> 添加節點X ----> 左旋轉
      //
      // 狀況一 若是紅黑樹的根節點爲空
      // 那麼 步驟一
      // 狀況二 若是紅黑樹的根節點不爲空 新添加的節點小於根節點
      // 那麼 步驟一 --> 步驟三
      // 狀況三 若是紅黑樹的根節點不爲空 新添加的節點大於根節點
      // 那麼 步驟一 --> 步驟二 --> 步驟三
      
      // 融合三節點
      //
      // 步驟1 步驟2 步驟3 步驟4 步驟5
      // [node] [node] [node] [Y] {Y}
      // / ----> / ----> / ----> / \ ----> / \
      //{X} {X} {Y} {X} {node} [X] [node]
      // \ /
      // {Y} {X}
      // 添加節點Y ----> 左旋轉 ----> 右旋轉 ----> 顏色翻轉
      //
      // 狀況一 若是新添加的這個新節點 大於 根節點
      // 那麼步驟1 --> 步驟4 --> 步驟5
      // 狀況二 若是新添加的這個節點比根節點要小而且比根節點的左孩子要小
      // 那麼步驟1 --> 步驟3 --> 步驟4 --> 步驟5
      // 狀況三 若是新添加的這個節點比根節點要小而且比根節點的左孩子要大
      // 那麼步驟1 --> 步驟2 --> 步驟3 --> 步驟4 --> 步驟5
      複製代碼
  3. 紅黑樹添加節點時維護性質的時機

    1. 紅黑樹維護的時機和 AVL 樹同樣,
    2. 先使用二分搜索樹的基本邏輯將新的節點添加進紅黑樹中,
    3. 以後再來進行回溯向上維護的操做,
    4. 也就是將當前的節點維護以後,再將維護後的新的節點返回給遞歸調用的上一層,
    5. 而後再到上一層中進行維護,再將維護後的新的節點返回給遞歸調用的上一層,
    6. 整個過程依此類推。

代碼示例

  1. MyRedBlackTree

    // 自定義紅黑樹節點 RedBalckTreeNode
    class MyRedBalckTreeNode {
       constructor(key = null, value = null, left = null, right = null) {
          this.key = key;
          this.value = value;
          this.left = left;
          this.right = right;
          this.color = MyRedBlackTree.RED; // MyRedBlackTree.BLACK;
       }
    
       // @Override toString 2018-11-25-jwl
       toString() {
          return (
             this.key.toString() +
             '--->' +
             this.value.toString() +
             '--->' +
             (this.color ? '紅色節點' : '綠色節點')
          );
       }
    }
    
    // 自定義紅黑樹 RedBlackTree
    class MyRedBlackTree {
       constructor() {
          MyRedBlackTree.RED = true;
          MyRedBlackTree.BLACK = false;
    
          this.root = null;
          this.size = 0;
       }
    
       // 判斷節點node的顏色
       isRed(node) {
          // 定義:空節點顏色爲黑色
          if (!node) return MyRedBlackTree.BLACK;
    
          return node.color;
       }
    
       // node x
       // / \ 左旋轉 / \
       // T1 x ---------> node T3
       // / \ / \
       // T2 T3 T1 T2
       leftRotate(node) {
          const x = node.right;
    
          // 左旋轉過程
          node.right = x.left;
          x.left = node;
    
          // 染色過程
          x.color = node.color;
          node.color = MyRedBlackTree.RED;
    
          // 返回這個 x
          return x;
       }
    
       // 顏色翻轉 當前節點變紅 左右孩子變黑
       // 表示當前節點須要繼續向上進行融合
       flipColors(node) {
          node.color = MyRedBlackTree.RED;
          node.left.color = MyRedBlackTree.BLACK;
          node.right.color = MyRedBlackTree.BLACK;
       }
    
       // node x
       // / \ 右旋轉 / \
       // x T2 -------> y node
       // / \ / \
       // y T1 T1 T2
       rightRotate(node) {
          const x = node.left;
    
          // 右翻轉過程
          node.left = x.right;
          x.right = node;
    
          // 染色過程
          x.color = node.color;
          node.color = MyRedBlackTree.RED;
    
          // 返回這個 x
          return x;
       }
    
       // 比較的功能
       compare(keyA, keyB) {
          if (keyA === null || keyB === null)
             throw new Error("key is error. key can't compare.");
          if (keyA > keyB) return 1;
          else if (keyA < keyB) return -1;
          else return 0;
       }
    
       // 根據key獲取節點 -
       getNode(node, key) {
          // 先解決最基本的問題
          if (!node) return null;
    
          // 開始將複雜的問題 逐漸縮小規模
          // 從而求出小問題的解,最後構建出原問題的解
          switch (this.compare(node.key, key)) {
             case 1: // 向左找
                return this.getNode(node.left, key);
                break;
             case -1: // 向右找
                return this.getNode(node.right, key);
                break;
             case 0: // 找到了
                return node;
                break;
             default:
                throw new Error(
                   'compare result is error. compare result : 0、 一、 -1 .'
                );
                break;
          }
       }
    
       // 添加操做 +
       add(key, value) {
          this.root = this.recursiveAdd(this.root, key, value);
          this.root.color = MyRedBlackTree.BLACK;
       }
    
       // 添加操做 遞歸算法 -
       recursiveAdd(node, key, value) {
          // 解決最簡單的問題
          if (!node) {
             this.size++;
             return new MyRedBalckTreeNode(key, value);
          }
    
          // 將複雜的問題規模逐漸變小,
          // 從而求出小問題的解,從而構建出原問題的答案
          if (this.compare(node.key, key) > 0)
             node.left = this.recursiveAdd(node.left, key, value);
          else if (this.compare(node.key, key) < 0)
             node.right = this.recursiveAdd(node.right, key, value);
          else {
             node.value = value;
             return node;
          }
    
          // 紅黑樹性質的維護
          // 是否須要左旋轉
          // 若是當前節點的右孩子是紅色 而且 左孩子不是紅色
          if (this.isRed(node.right) && !this.isRed(node.left))
             node = this.leftRotate(node);
    
          // 是否須要右旋轉
          // 若是當前節點的左孩子是紅色 而且 左孩子的左孩子也是紅色
          if (this.isRed(node.left) && this.isRed(node.left.left))
             node = this.rightRotate(node);
    
          // 是否須要顏色的翻轉
          // 當前節點的左孩子和右孩子全都是紅色
          if (this.isRed(node.left) && this.isRed(node.right))
             this.flipColors(node);
    
          // 最後返回這個node
          return node;
       }
    
       // 刪除操做 返回被刪除的元素 +
       remove(key) {
          let node = this.getNode(this.root, key);
          if (!node) return null;
    
          this.root = this.recursiveRemove(this.root, key);
          return node.value;
       }
    
       // 刪除操做 遞歸算法 +
       recursiveRemove(node, key) {
          // 解決最基本的問題
          if (!node) return null;
    
          if (this.compare(node.key, key) > 0) {
             node.left = this.recursiveRemove(node.left, key);
             return node;
          } else if (this.compare(node.key, key) < 0) {
             node.right = this.recursiveRemove(node.right, key);
             return node;
          } else {
             // 當前節點的key 與 待刪除的key的那個節點相同
             // 有三種狀況
             // 1. 當前節點沒有左子樹,那麼只有讓當前節點的右子樹直接覆蓋當前節點,就表示當前節點被刪除了
             // 2. 當前節點沒有右子樹,那麼只有讓當前節點的左子樹直接覆蓋當前節點,就表示當前節點被刪除了
             // 3. 當前節點左右子樹都有, 那麼又分兩種狀況,使用前驅刪除法或者後繼刪除法
             // 1. 前驅刪除法:使用當前節點的左子樹上最大的那個節點覆蓋當前節點
             // 2. 後繼刪除法:使用當前節點的右子樹上最小的那個節點覆蓋當前節點
    
             if (node.left === null) {
                let rightNode = node.right;
                node.right = null;
                this.size--;
                return rightNode;
             } else if (node.right === null) {
                let leftNode = node.left;
                node.left = null;
                this.size--;
                return leftNode;
             } else {
                let predecessor = this.maximum(node.left);
                node.left = this.removeMax(node.left);
                this.size++;
    
                // 開始嫁接 當前節點的左右子樹
                predecessor.left = node.left;
                predecessor.right = node.right;
    
                // 將當前節點從根節點剔除
                node = node.left = node.right = null;
                this.size--;
    
                // 返回嫁接後的新節點
                return predecessor;
             }
          }
       }
    
       // 刪除操做的兩個輔助函數
       // 獲取最大值、刪除最大值
       // 之前驅的方式 來輔助刪除操做的函數
    
       // 獲取最大值
       maximum(node) {
          // 不再能往右了,說明當前節點已是最大的了
          if (!node.right) return node;
    
          // 將複雜的問題漸漸減少規模,從而求出小問題的解,最後用小問題的解構建出原問題的答案
          return this.maximum(node.right);
       }
    
       // 刪除最大值
       removeMax(node) {
          // 解決最基本的問題
          if (!node.right) {
             let leftNode = node.left;
             node.left = null;
             this.size--;
             return leftNode;
          }
    
          // 開始化歸
          node.right = this.removeMax(node.right);
          return node;
       }
    
       // 查詢操做 返回查詢到的元素 +
       get(key) {
          let node = this.getNode(this.root, key);
          if (!node) return null;
          return node.value;
       }
    
       // 修改操做 +
       set(key, value) {
          let node = this.getNode(this.root, key);
          if (!node) throw new Error(key + " doesn't exist.");
    
          node.value = value;
       }
    
       // 返回是否包含該key的元素的判斷值 +
       contains(key) {
          return this.getNode(this.root, key) !== null;
       }
    
       // 返回映射中實際的元素個數 +
       getSize() {
          return this.size;
       }
    
       // 返回映射中是否爲空的判斷值 +
       isEmpty() {
          return this.size === 0;
       }
    
       // @Override toString() 2018-11-05-jwl
       toString() {
          let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `;
          document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [ <br/><br/>`;
    
          // 以非遞歸的前序遍歷 輸出字符串
          let stack = new MyLinkedListStack();
    
          stack.push(this.root);
    
          if (this.root === null) stack.pop();
    
          while (!stack.isEmpty()) {
             let node = stack.pop();
    
             if (node.left !== null) stack.push(node.left);
             if (node.right !== null) stack.push(node.right);
    
             if (node.left === null && node.right === null) {
                mapInfo += ` ${node.toString()} \r\n`;
                document.body.innerHTML += ` ${node.toString()} <br/><br/>`;
             } else {
                mapInfo += ` ${node.toString()}, \r\n`;
                document.body.innerHTML += ` ${node.toString()}, <br/><br/>`;
             }
          }
    
          mapInfo += ` ] \r\n`;
          document.body.innerHTML += ` ] <br/><br/>`;
    
          return mapInfo;
       }
    }
    複製代碼

紅黑樹的性能測試

  1. 測試用例的結果
    1. 結論是,
    2. 並不是在任何狀況下越複雜的算法或者看起來複雜度更低的算法就是更好的,
    3. 不少狀況下對於比較簡單的數據,或者對於比較少的數據,
    4. 可能簡單的算法反而是更快一點,歸併排序算法雖然是O(nlogn)級別的,
    5. 可是在數據規模比較小的狀況下,頗有可能插入排序法纔是最優秀的選擇,
    6. 紅黑樹自己並非一個嚴格的平衡樹,紅黑樹從根到葉子節點的高度最多會到 2logn,
    7. 因此是比 avl 樹要高一些的,因此在查詢這個操做中,紅黑樹並不佔優點,
    8. 紅黑樹真正佔優點的操做在於添加或者刪除這兩個操做上,
    9. 若是結合添加、刪除、查詢這三個操做,那麼總體上紅黑樹的性能要比 AVL 樹高一點,
    10. 若是比較偏重查詢的過程,紅黑樹的優點體現不出來,
    11. 紅黑樹的實現其實還能夠更加的優化一下。

性能測試總結

  1. 對於徹底隨機的數據,普通的二分搜索樹不會退化成一個鏈表
    1. 因此仍是很好用的,它的高度相對保持的比較好,
    2. 它的內部也不會有一些比較複雜的維持平衡的代碼,
    3. 因而在必定程度上下降了它的開銷。
    4. 缺點是在極端狀況下會退化成鏈表,或者高度嚴重不平衡。
  2. 對於查詢和修改操做比較多的使用狀況,AVL 樹比較好用。
  3. 對於添加刪除操做比較多的使用狀況,紅黑樹比較好用。
    1. 紅黑樹犧牲了平衡性(2logn 的高度),
    2. 因此紅黑樹並不徹底知足平衡二叉樹的定義 ,
    3. 這個高度確定是比 AVL 樹要高的,
    4. 紅黑樹的統計性能更優(綜合增刪改查全部的操做),
    5. 也正是如此,不少語言內部的容器類所實現有序的映射和集合,
    6. 好比 系統內置 的 Map、Set 的底層都是紅黑樹的,
    7. 紅黑樹自己仍是一個二分搜索樹,因此它也是有序的。
  4. 對於時間複雜度分析來講
    1. AVL 樹與紅黑樹總體是在一個級別的,
    2. 可是平均的性能是紅黑樹比 AVL 樹要高一點,
    3. 紅黑樹的添加和刪除操做相對 AVL 樹來講更有優點一些。
  5. 二分搜索樹是有序性的
    1. 能夠很方便的找到最大值、最小值、一個元素的前驅和後繼等等,
    2. 因此基於二分搜索樹實現的其它樹如 AVL、紅黑樹都是有序的。

代碼示例

  1. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('RedBlackTree Comparison Area');
          const n = 2000000;
    
          const myBSTMap = new MyBinarySearchTreeMap();
          const myAVLTree = new MyAVLTree();
          const myRedBlackTree = new MyRedBlackTree();
          let performanceTest1 = new PerformanceTest();
    
          const random = Math.random;
          let arrNumber = new Array(n);
    
          // 循環添加隨機數的值
          for (let i = 0; i < n; i++) arrNumber[i] = Math.floor(n * random());
    
          this.alterLine('MyBSTMap Comparison Area');
          const myBSTMapInfo = performanceTest1.testCustomFn(function() {
             // 添加
             for (const word of arrNumber)
                myBSTMap.add(word, String.fromCharCode(word));
          });
    
          // 總毫秒數:4771
          console.log(myBSTMapInfo + ' 節點個數:' + myBSTMap.getSize());
          this.show(myBSTMapInfo + ' 節點個數:' + myBSTMap.getSize());
    
          this.alterLine('MyAVLTree Comparison Area');
          // const that = this;
          const myAVLTreeInfo = performanceTest1.testCustomFn(function() {
             for (const word of arrNumber)
                myAVLTree.add(word, String.fromCharCode(word));
          });
    
          // 總毫秒數:6226
          console.log(myAVLTreeInfo + ' 節點個數:' + myAVLTree.getSize());
          this.show(myAVLTreeInfo + ' 節點個數:' + myAVLTree.getSize());
    
          this.alterLine('MyRedBlackTree Comparison Area');
    
          const myRedBlackTreeInfo = performanceTest1.testCustomFn(function() {
             for (const word of arrNumber)
                myRedBlackTree.add(word, String.fromCharCode(word));
          });
    
          // 總毫秒數:6396
          console.log(
             myRedBlackTreeInfo + ' 節點個數:' + myRedBlackTree.getSize()
          );
          this.show(
             myRedBlackTreeInfo + ' 節點個數:' + myRedBlackTree.getSize()
          );
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

更多和紅黑樹相關的話題

  1. 紅黑樹中刪除節點
    1. 紅黑樹中的刪除節點操做比添加節點操做還要複雜的多,
    2. 在面試中讓你實現一個刪除節點的操做幾乎是不可能的,
    3. 就連添加節點的操做都不多讓你實現,
    4. 只要是看你知不知道紅黑樹的本質是怎樣的,
    5. 紅黑樹的本質就是,
    6. 紅黑樹與二三樹之間是等價的,紅黑樹知足的那五條基本性質,
    7. 紅黑樹本質上是保持黑平衡的這樣的一種數據結構,
    8. 它從某種程度上來說犧牲了平衡性,
    9. 可是它的統計性能更優,添加及刪除性能比 AVL 樹好,
    10. 紅黑樹的添加操做是分各類狀況來進行處理的,
    11. 紅黑樹的刪除操做也是分各類狀況來進行處理的,
    12. 刪除節點的各類狀況處理方式更加複雜,
    13. 就連紅黑樹發明人寫的算法 4 裏面都沒有對紅黑樹的刪除操做的邏輯進行一個詳細的介紹,
    14. 刪除操做的邏輯分雜瑣碎,因此邏輯很是的複雜。
  2. 左傾紅黑樹和右傾紅黑樹
    1. 本身實現的紅黑樹屬於左傾紅黑樹,
    2. 也是相對比較標準的一種紅黑樹,
    3. 也能夠實現右傾紅黑樹,也就是紅色節點做爲右孩子節點,
    4. 實現出右傾紅黑樹就可以讓你對紅黑樹內部的邏輯及運行機制有一個至關深入的理解。
  3. 統計性能更優的樹
    1. 紅黑樹是一種統計性能更優的樹,
    2. 還有另一種統計性能更優的樹結構叫作伸展樹(SplayTree),
    3. 它的本質也是一種二叉樹,它也能夠維持自平衡,
    4. 這種樹沒有像 AVL 樹那樣嚴格的定義,它更重要的一點是運用了局部性的原理,
    5. 它假設一般在數據結構中存儲的數據,
    6. 若是剛剛被訪問的內容下次會有更高的機率去訪問它,
    7. 基於這樣的一個假設,
    8. 對這個樹建立的過程以及查詢、修改操做都會對這個樹的結構進行必定的變化,
    9. 伸展樹的實現要比紅黑樹的實現要簡單。
  4. 基於紅黑樹的 Map 和 Set
    1. 系統內置的的 Map 和 Set 就是基於紅黑樹的。
  5. 紅黑樹的其它實現
    1. 本身實現的紅黑樹並非紅黑樹惟一的實現方式,
    2. 雖然總體邏輯上原理是同樣的,可是在具體實現上能夠有更優化的一些實現方式,
    3. 算法導論中也有對紅黑樹的實現的描述,
    4. 面試中不會去考察紅黑樹實現的邏輯中某一個具體細節。
相關文章
相關標籤/搜索