【從蛋殼到滿天飛】JS 數據結構解析和算法實現-AVL樹(一)

思惟導圖

前言

【從蛋殼到滿天飛】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

平衡二叉樹與 AVL 樹

  1. 在以前實現的二分搜索樹的問題算法

    1. 當你按照順序添加[1, 2, 3, 4, 5, 6]後,
    2. 二分搜索樹會退化成一條鏈表,
    3. 由於你第一個添加的 1 成爲了根節點,
    4. 二分搜索樹的性質是,當前節點的左子樹小於當前節點,
    5. 當前節點的右子樹大於當前節點,
    6. 後面的值都是按照+1 的順序增大,
    7. 那麼都會是在新增長的節點的右子樹上不斷新增節點,
    8. 這樣就會造成一棵向右擴張的樹,這棵樹就像鏈表同樣。
  2. 在現有的二分搜索樹中添加一些機制,從而可以維持平衡二叉樹的性質,編程

    1. AVL 樹就是一種最爲經典的平衡二叉樹,
    2. 這種樹之因此叫作 AVL 樹是由於這三個字母是 AVL 樹的發明人
    3. G.M.Adelson-Velsky 和 E.M.Landis,
    4. 就是這兩個發明人的名字的首字母的縮寫,
    5. 誒地歐斯-喂斯 K 和蘭迪斯這兩我的,
    6. 這兩我的都是俄羅斯的計算機科學家,
    7. AVL 這種樹結構是由它們在 1962 年首次發表的論文中提出的,
    8. 一般都認爲 AVL 樹是一種最先的自平衡二分搜索樹結構。
  3. 平衡二叉樹數組

    1. 一棵滿的二叉樹必定是一棵平衡二叉樹,
    2. 滿的二叉樹的高度可讓整棵樹的高度達到最低的狀態,
    3. 所謂的滿二叉樹是指,除了葉子節點以外,
    4. 全部的其它的節點都有左右兩個子樹,一般數據不會填滿一棵二叉樹。
  4. 在本身實現的堆中引入了徹底二叉樹的概念,數據結構

    1. 對於一棵徹底二叉樹來講,
    2. 就是將全部的元素按照二叉樹的形狀一層一層的鋪開,
    3. 最終獲得的結果就是一棵徹底的二叉樹,對於徹底的二叉樹來講,
    4. 有可能有一個非葉子的節點它的右子樹是空的,
    5. 總體而言對於徹底二叉樹來講,
    6. 它空缺節點的部分必定在整棵樹的右下部分,
    7. 相應的對於一棵徹底二叉樹來講,
    8. 整棵樹的葉子節點最大的深度值和最小的深度值相差不會超過一,
    9. 也就是說全部的葉子節點要否則就是在這棵樹的最後一層,
    10. 要否則就是在這棵樹的倒數第二層。
  5. 本身實現的線段樹也是一種平衡二叉樹框架

    1. 雖然它不是一個徹底二叉樹,
    2. 這是由於線段樹空出來的地方不必定是在整棵樹的右下角的位置,
    3. 可是總體在一棵線段樹上,
    4. 葉子節點或者在最後一層或者在倒數第二層,
    5. 那麼對於整棵樹來講全部的葉子節點的深度相差不會超過一。
  6. 不管是堆仍是線段樹都是平衡二叉樹的例子dom

    1. 不管是滿二叉樹、徹底二叉樹、線段樹這樣的一種平衡二叉樹,
    2. 葉子節點它們之間的差距不會超過一,
    3. 相對來講都是比較理想的狀況,這樣的二叉樹它的平衡是很是高的,
    4. 在 AVL 樹中維護的這種平衡二叉樹它的條件更加寬鬆一些,
  7. 在 AVL 樹中定義平衡二叉樹

    1. 對於任意一個節點來講,
    2. 它的左子樹的高度和右子樹的高度相差不能超過一,
    3. 這句話和以前實現的堆的徹底二叉樹仍是線段樹這樣的一種二叉樹
    4. 差很少,
    5. 可是實際上他們是有區別的,對於堆和線段樹來講,
    6. 能夠保證任意一個葉子節點相應的高度差都不能超過一,
    7. 而 AVL 定義的任意一個節點它的左右子樹高度差不能超過一,
    8. 在這個定義下相應的獲得的這棵平衡二叉樹,
    9. 有可能看起來不是那麼的平衡,由於它只是知足 AVL 定義的平衡二叉樹,
    10. 顯然這樣的狀況不會出如今堆或者線段樹這兩種樹結構中,
    11. 這種樹看起來稍微有一點的偏斜,可是仔細的去驗證每個節點,
    12. 會發現它是知足這個條件的,
    13. 任意一個節點的左子樹和右子樹的高度差並無超過一,
    14. 以下圖,節點 12 的左子樹的高度爲 3,右子樹的高度爲 2,
    15. 節點 12 的左右子樹高度差沒有超過一;節點 8 的左子樹高度爲 2,
    16. 右子樹的高度 1,因此節點 8 的左右子樹高度差沒有超過一;
    17. 節點 18 的左子樹的高度爲 1,右子樹的高度爲 0,
    18. 因此節點 18 的左右子樹的高度差沒有超過一;
    19. 其它的節點也是同樣的性質,雖然這棵樹好像有一些偏斜,
    20. 可是 AVL 定義下的一棵平衡二叉樹,對於你定義的這種平衡二叉樹,
    21. 他也是知足平衡二叉樹的高度和節點數量之間的關係是O(logn)級別的。
    // 知足AVL定義的平衡二叉樹
    
    // (12)
    // / \
    // (8) (18)
    // /\ /
    // (5)(11)(17)
    // /
    // (4)
    複製代碼
  8. 對上面圖中的 AVL 平衡二叉樹進行添加節點

    1. 好比添加一個節點 2,根據原來本身實現的二分搜索樹的性質,
    2. 那麼這個節點 2 就會從根節點開始一路找下來,
    3. 最終添加到節點 4 的左子樹的位置,
    4. 相應的如何再添加一個節點 7,這個節點 7 會被添加到節點 5 的右子樹中,
    5. 當樹變成這個樣子,那麼就再也不是一棵 AVL 平衡二叉樹了,
    6. 在節點 8 這個位置,左子樹的高度是 3,而右子樹的高度是 1,
    7. 左右子樹的高度差爲 2,因此打破了 AVL 平衡二叉樹的條件,
    8. 同理在節點 12 這個位置,左子樹的高度是 4,而右子樹的高度是 2,
    9. 左右子樹的高度差爲 2,也打破了這個條件,
    10. 因此如今所繪製的這棵二叉樹再也不是一棵 AVL 平衡二叉樹。
    // 添加節點2和節點7後的樹
    
    // (12)
    // / \
    // (8) (18)
    // / \ /
    // (5) (11) (17)
    // / \
    // (4) (7)
    // /
    // (2)
    //
    複製代碼
  9. 讓上面圖中的樹維持一個平衡

    1. 爲了讓上面圖中的樹保持一個平衡,
    2. 那麼必須保證在插入節點的時候相應的也要顧及這棵樹右側的部分,
    3. 由於如今整棵樹看起來是向左偏斜的,
    4. 必須相應的填補這棵樹右側的空間上的一些節點,
    5. 只有這樣纔可以讓整棵樹繼續維持這個 AVL 平衡二叉樹的
    6. 左右子樹高度差不超過一這樣的性質,
    7. 因此基於這樣定義下的平衡二叉樹來講,雖然有可能看起來有一些偏斜,
    8. 可是實際上爲了維持住這個性質,
    9. 那麼整棵平衡二叉樹左右兩邊都必須有必定數量的節點,
    10. 而不可能太過偏斜,而上圖中的那棵樹就是太過偏斜了,
    11. 致使打破了這棵 AVL 平衡二叉樹左右子樹高度差不超過一這樣的性質,
    12. 因此總體而言,在這個性質下,
    13. 也能夠保證這棵二叉樹它的高度和節點數量之間的關係是O(logn)這個級別的,
    14. 在具體編程過程當中因爲跟蹤每個節點的高度是多少,
    15. 只有這樣才方便來判斷當前的這棵二叉樹是不是平衡的。
  10. 對於以前所實現的二分搜索樹來講,

  11. 實現這個 AVL 平衡二叉樹相應的每個節點都記錄一下這個節點所在的高度,

  12. 其實這個記錄很是的簡單,對於葉子節點來講它所對應的高度就是 1,

  13. 也就是節點 2 對應的高度爲 1;節點 4 下面只有一個葉子節點,那麼對應的高度就是 2;

  14. 節點 7 也是一個葉子節點,因此它的高度值也是 1;

  15. 可是對於節點 5 來講它有左右兩棵子樹,左邊的子樹高度爲 2,右邊的子樹高度爲 1,

  16. 相應的節點 5 這個節點它的高度就是左右兩棵子樹中最高的那棵樹再加上自身的 1,

  17. 那麼這樣一來節點 5 它的高度就是 2+1=3;很是好理解,節點 11 它的高度就是 1,

  18. 其它的如此類推,以下圖的標註所示。

    // 節點名稱加高度 小括號中的是節點名稱,中括號中的是高度值
    
    // 【5】(12)
    // / \
    // 【4】(8) (18)【2】
    // / \ /
    // 【3】(5) 【1】(11) (17)【1】
    // / \
    // 【2】(4) (7)【1】
    // /
    // 【1】(2)
    //
    複製代碼
  19. 平衡因子

  20. 對整棵樹每個節點都標註上高度值以後,

  21. 相應的計算一個稱之爲平衡因子的這樣的一個數,

  22. 這個名詞雖然聽起來比較複雜,實際上很是的簡單,

  23. 它就是指對於每個節點而言,它的左右子樹的高度差,

  24. 計算高度值的差可使用左子樹的高度減去右子樹的高度差。

  25. 對於節點 2 來講它是要給葉子節點,

  26. 它的左右兩棵子樹至關因而兩棵空樹,空樹的高度值能夠記爲 0,

  27. 相應的葉子節點的平衡因子其實就是0 - 0 = 0,最後的結果也爲 0;

  28. 對於節點 4 來講它的左子樹對應的高度值爲 1,右子樹爲空所對應的高度值爲 0,

  29. 那麼它的平衡因子就是1 - 0 = 1,最後的結果爲 1;

  30. 節點 7 是個葉子節點,那麼它的平衡因子就是 0;

  31. 節點 5 兩棵子樹的高度差是2 - 1 = 1,最終的結果爲 1;

  32. 節點 11 是葉子節點,因此他的平衡因子爲 0;

  33. 節點 8 的兩棵子樹的高度查實3 - 1 = 2,最終的結果爲 2。

  34. 這就意味着節點 8 這個左右子樹的高度差已經超過了一,

  35. 經過這個平衡因子就能夠看出來這棵樹已經不是一棵平衡二叉樹了,

  36. 以下圖這樣標註出平衡因子以後,

  37. 一旦在一棵樹中有一個節點它的平衡因子是大於等於 2 或者小於等於-2 的,

  38. 換句話說它的絕對值是等於 2,那麼整棵樹就再也不是一棵平衡二叉樹了。

  39. 以下圖所示,節點 12 和節點 8 的平衡因子的絕對值都是 2,

  40. 那麼就說明這兩個節點破壞了平衡二叉樹對應的那個性質,

  41. 這個對應的性質就是 左右子樹的高度差超過了一,

  42. 一旦能夠計算出每個節點的平衡因子,

  43. 相應的也能夠看出來當前的這棵樹是不是一棵平衡二叉樹,

  44. 不只如此在後續的 AVL 實現中也會藉助每個節點

  45. 它對應的平衡因子來決定是否要進行一些特殊的操做,

  46. 從而來維持整棵樹是不是一棵平衡二叉樹。

    // 1. 節點名稱 加高度值 加平衡因子值
    // 1. 小括號中的是節點名稱,
    // 2. 中括號中的是高度值,
    // 3. 大括號中的是平衡因子
    //
    // 2. 平衡因子值 = 左右子樹的高度差
    // 3. 左右子樹的高度差 = 左子樹的高度值 - 右子樹的高度值
    
    // {2}【5】(12)
    // / \
    // {2}【4】(8) (18)【2】{1}
    // / \ /
    // {1}【3】(5) {0}【1】(11)(17)【1】{0}
    // / \
    // {1}【2】(4) (7)【1】{1}
    // /
    // {0}【1】(2)
    //
    複製代碼
  47. 經過對每個節點標註相應的高度

  48. 而後經過這個高度很是簡單的計算出平衡因子,

  49. 最後在經過平衡因子來決定是否進行一些特殊的操做,

  50. 從而維持整棵樹是一棵平衡二叉樹的性質。

計算節點的高度和平衡因子

  1. 對原先實現的二分搜索樹的代碼進行修改
    1. 讓它能夠記錄每個節點的高度值,
    2. 同時來處理來計算這個平衡因子。
  2. AVL 樹總體的框架和二分搜索樹映射版類似。
    1. 因此 MyBSTMap 中的代碼能夠拿過來修改一下,
    2. 總體 AVL 樹就是在二分搜索樹代碼的基礎上補充一些代碼,
    3. 其實就是添加上自平衡的機制,
    4. 使得原先實現的二分搜索樹在對節點進行操做的時候能夠保證整棵樹是平衡的,
    5. 這裏平衡的定義就是對於每個節點左右子樹的高度差不超過一。
  3. 在 AVLTree 中進行修改
    1. 在 AVLTree 的 Node 中添加一個新屬性 height,
    2. 同時要對這個新的成員變量進行維護,也就是新增一個 getHeight 方法,
    3. 傳入一個節點而後返回這個節點對應的高度值;
    4. 添加方法中,每添加一個節點,不管是在左子樹中進行添加,
    5. 仍是在右子樹中進行添加,添加完了這個節點以後,
    6. 對於當前的這個 node 來講它的 height 都有可能會發生變化,
    7. 這是由於對於當前的 node 來講無論是左子樹仍是右子樹來講,
    8. 多了一個節點以後,相應的它自身的高度就有可能發生變化,
    9. 因此須要對當前的這個 node 的 height 進行一個更新,
    10. 也就是 1 + 左右子樹中 height 值最大的那個 height 值,
    11. 也就是當前節點的 height 等於本身左右子樹中最大的那個高度值;
    12. 更新了節點的高度值以後,就能夠計算平衡因子了,
    13. 也就是當前節點的左子樹的高度減去當前右子樹的高度,
    14. 取高度的差的絕對值,若是大於 1,那麼就說明不符合 AVL 平衡二叉樹的性質了。
  4. 經過對不符合要求的平衡因子值後,你會發現二分搜索樹中不符合要求的節點很是多,
    1. 雖然在以前的測試中二分搜索樹的性能已經很好了,可是在隨機性的角度出發,
    2. 不符合要求的平衡因子也是很是多的,也就是說仍是會出現高度的不平衡的,
    3. 因此也就是說仍是有很大的優化空間的。

代碼示例

  1. AVLTree

    // 自定義AVL樹節點 AVLTreeNode
    class MyAVLTreeNode {
       constructor(key = null, value = null, left = null, right = null) {
          this.key = key;
          this.value = value;
          this.left = left;
          this.right = right;
          this.height = 1;
       }
    
       // @Override toString 2018-11-24-jwl
       toString() {
          return this.key + '--->' + this.value + '--->' + this.height;
       }
    }
    
    // 自定義AVL樹 AVLTree
    class MyAVLTree {
       constructor() {
          this.root = null;
          this.size = 0;
       }
    
       // 比較的功能
       compare(keyA, keyB) {
          if (keyA === null || keyB === null)
             throw new Error("key is error. key can't compare.");
          if (keyA > keyB) return 1;
          else if (keyA < keyB) return -1;
          else return 0;
       }
    
       // 獲取某個節點的高度 -
       getHeight(node) {
          // 節點爲空 返回0
          if (!node) return 0;
    
          // 直接返回這個節點的高度
          return node.height;
       }
    
       // 獲取一個節點的平衡因子 -
       getBalanceFactor(node) {
          // 節點爲空 返回0
          if (!node) return 0;
    
          // 左右子樹的高度值
          const leftHeight = this.getHeight(node.left);
          const rightHeight = this.getHeight(node.right);
    
          // 左子樹的高度 - 右子樹高度的值 = 平衡因子
          return leftHeight - rightHeight;
       }
    
       // 根據key獲取節點 -
       getNode(node, key) {
          // 先解決最基本的問題
          if (node === null) return null;
    
          // 開始將複雜的問題 逐漸縮小規模
          // 從而求出小問題的解,最後構建出原問題的解
          switch (this.compare(node.key, key)) {
             case 1: // 向左找
                return this.getNode(node.left, key);
                break;
             case -1: // 向右找
                return this.getNode(node.right, key);
                break;
             case 0: // 找到了
                return node;
                break;
             default:
                throw new Error(
                   'compare result is error. compare result : 0、 一、 -1 .'
                );
                break;
          }
       }
    
       // 添加操做 +
       add(key, value) {
          this.root = this.recursiveAdd(this.root, key, value);
       }
    
       // 添加操做 遞歸算法 -
       recursiveAdd(node, key, value) {
          // 解決最簡單的問題
          if (node === null) {
             this.size++;
             return new MyAVLTreeNode(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;
    
          // 在這裏對節點的高度進行從新計算 節點自己高度爲1
          // 計算方式: 1 + 左右子樹的height值最大的那個height值
          node.height =
             1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
    
          // 計算一個節點的平衡因子
          const balanceFactor = this.getBalanceFactor(node);
    
          // 若是平衡因子的絕對值大於1 說明不知足AVL平衡二叉樹的性質了
          if (Math.abs(balanceFactor) > 1) {
             console.log(
                node.toString() + ' unbalanced : ' + balanceFactor + '\r\n'
             );
             document.body.innerHTML +=
                node.toString() + ' unbalanced : ' + balanceFactor + '<br/>';
          }
    
          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;
       }
    }
    複製代碼
  2. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('AVLTree Area');
          // 千級別
          const openCount = 100; // 操做數
          const rank = 10000000;
    
          // 生成同一份測試數據的輔助代碼
          const random = Math.random;
          const array = new Array(openCount);
    
          // 生成同一份測試數據
          for (var i = 0; i < openCount; i++)
             array[i] = Math.floor(random() * rank);
    
          // 建立AVL樹實例
          const avl = new MyAVLTree();
    
          for (const value of array) avl.add(value);
       }
    
       // 將內容顯示在頁面上
       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. 本身實現的二分搜索樹不符合要求 AVL 平衡二叉樹性質的節點很是多,
    1. 因此會出現高度的不平衡,也就說明了有很大的優化空間,
    2. 這個優化空間就是,讓本身實現的 AVL 樹能夠維持自平衡。
  2. 改進的方式和原理
    1. 須要對節點進行一下判斷,
    2. 是否是二分搜索樹同時是否是平衡二叉樹,
    3. 對於 AVL 樹來講它是對二分搜索樹的一個改進,
    4. 改進的地方是二分搜索樹有可能會退化爲一個鏈表的這種狀況,
    5. 由於就引入了平衡因子這個概念,
    6. AVL 樹要保持對於每個節點來講左右子樹的高度差不能超過一,
    7. 可是在這種狀況下要注意,AVL 樹同時也是一個二分搜索樹,
    8. 因此它也要知足二分搜索樹的性質,也就是對於每個節點來講,
    9. 左子樹全部的節點都要小於這個節點的值,右子樹全部的節點都要大於這個節點的值,
    10. 也就是說它的左右子樹依然是二分搜索樹,那麼在添加 AVL 樹自平衡的機制的時候,
    11. 若是你的代碼有問題的話頗有可能會打破這個性質,因此能夠設置一個函數,
    12. 可以方便的檢查當前的這棵 AVL 樹它是否是依然能夠保證它是一個棵二分搜索樹。
    13. 經過二分搜索樹的順序性,也就是中序遍歷,而後將結果存到一個動態數組中,
    14. 數組中的元素會是從小到大升序的排列,
    15. 以循環的方式檢查這個數組中前一個元素值是否比當前元素值小,
    16. 若是符合要求就證實這就是一棵二分搜索樹,
    17. 不然就不是,經過返回相應的判斷值便可。
    18. 除了要判斷這棵樹是不是一棵二分搜索樹以外,還要判斷這棵樹是否是一棵平衡二叉樹,
    19. AVL 平衡二叉樹的定義是,對於每個節點的左右子樹的高度差不能超過一,
    20. 這個定義是一個遞歸的定義,須要從根節點開始判斷它的左右子樹的高度差不能超過一,
    21. 而後再去看它的左右子樹的根節點是否一樣知足這個條件,
    22. 因此須要又要寫一個遞歸輔助函數,這個輔助函數是之前序遍歷的方式
    23. 判斷當前節點的及其左右子樹是否符合平衡二叉樹的定義,
    24. 若是所有符合返回 true,不然就返回 false。
  3. 當你對 AVL 樹進行相應的操做以後依然能夠維持二分搜索樹和平衡二叉樹的性質
    1. 以上的操做都是準備工做,只有知足這樣的兩種性質才能算是 AVL 樹。

代碼示例

  1. AVLTree

    // 自定義AVL樹節點 AVLTreeNode
    class MyAVLTreeNode {
       constructor(key = null, value = null, left = null, right = null) {
          this.key = key;
          this.value = value;
          this.left = left;
          this.right = right;
          this.height = 1;
       }
    
       // @Override toString 2018-11-24-jwl
       toString() {
          return this.key + '--->' + this.value + '--->' + this.height;
       }
    }
    
    // 自定義AVL樹 AVLTree
    class MyAVLTree {
       constructor() {
          this.root = null;
          this.size = 0;
       }
    
       // 比較的功能
       compare(keyA, keyB) {
          if (keyA === null || keyB === null)
             throw new Error("key is error. key can't compare.");
          if (keyA > keyB) return 1;
          else if (keyA < keyB) return -1;
          else return 0;
       }
    
       // 獲取某個節點的高度 -
       getHeight(node) {
          // 節點爲空 返回0
          if (!node) return 0;
    
          // 直接返回這個節點的高度
          return node.height;
       }
    
       // 獲取一個節點的平衡因子 -
       getBalanceFactor(node) {
          // 節點爲空 返回0
          if (!node) return 0;
    
          // 左右子樹的高度值
          const leftHeight = this.getHeight(node.left);
          const rightHeight = this.getHeight(node.right);
    
          // 左子樹的高度 - 右子樹高度的值 = 平衡因子
          return leftHeight - rightHeight;
       }
    
       // 根據key獲取節點 -
       getNode(node, key) {
          // 先解決最基本的問題
          if (node === null) return null;
    
          // 開始將複雜的問題 逐漸縮小規模
          // 從而求出小問題的解,最後構建出原問題的解
          switch (this.compare(node.key, key)) {
             case 1: // 向左找
                return this.getNode(node.left, key);
                break;
             case -1: // 向右找
                return this.getNode(node.right, key);
                break;
             case 0: // 找到了
                return node;
                break;
             default:
                throw new Error(
                   'compare result is error. compare result : 0、 一、 -1 .'
                );
                break;
          }
       }
    
       // 添加操做 +
       add(key, value) {
          this.root = this.recursiveAdd(this.root, key, value);
       }
    
       // 添加操做 遞歸算法 -
       recursiveAdd(node, key, value) {
          // 解決最簡單的問題
          if (node === null) {
             this.size++;
             return new MyAVLTreeNode(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;
    
          // 在這裏對節點的高度進行從新計算 節點自己高度爲1
          // 計算方式: 1 + 左右子樹的height值最大的那個height值
          node.height =
             1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
    
          // // 計算一個節點的平衡因子
          // const balanceFactor = this.getBalanceFactor(node);
    
          // // 若是平衡因子的絕對值大於1 說明不知足AVL平衡二叉樹的性質了
          // if (Math.abs(balanceFactor) > 1) {
          // console.log(node.toString() + " unbalanced : " + balanceFactor + "\r\n");
          // document.body.innerHTML += node.toString() + " unbalanced : " + balanceFactor + "<br/>";
          // }
    
          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;
       }
    
       // 判斷當前這棵樹是不是一棵二分搜索樹,有二分搜索樹順序性
       isBanarySearchTree() {
          // 若是節點爲空 那麼這就是一棵空的二分搜索樹
          if (!this.root) return true;
    
          // 存儲二分搜索樹中的key
          const list = new Array();
    
          // 中序遍歷後,添加到list中的值會是以從小到大升序的樣子排列
          this.inOrder(this.root, list);
    
          // 從前日後判斷 list中的值是不是從小到大升序的排列
          // 驗證 當前樹是否符合二分搜索樹的性質
          for (var i = 1; i < list.length; i++)
             if (list[i - 1] > list[i]) return false;
          return true;
       }
    
       // 中序遍歷 輔助函數 -
       inOrder(node, list) {
          // 遞歸到底的狀況
          if (!node) return;
    
          // 中序遍歷時,添加到數組中的值會是以從小到大升序的樣子排列
          this.inOrder(node.left, list);
          list.push(node.key);
          this.inOrder(node.right, list);
       }
    
       // 判斷該二叉樹是否一棵平衡二叉樹
       isBalanced() {
          return this.recursiveIsBalanced(this.root);
       }
    
       // 遞歸判斷某一個節點是否符合平衡二叉樹的定義 輔助函數 -
       recursiveIsBalanced(node) {
          // 可以遞歸到底,說明符合要求
          // 空的節點左右孩子高度差確定爲0,
          // 由於空樹沒有左右子樹,更加談不上下面去判斷它的左右子樹高度差是否會超過一。
          if (!node) return true;
    
          // 若是當前節點的高度差大於1 說明不符合要求
          if (Math.abs(this.getBalanceFactor(node)) > 1) return false;
    
          // 遞歸的去判斷當前節點的 左右子樹是否符合要求
          return (
             this.recursiveIsBalanced(node.left) &&
             this.recursiveIsBalanced(node.right)
          );
       }
    
       // @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;
       }
    }
    複製代碼
  2. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('AVLTree Area');
          // 千級別
          const openCount = 100; // 操做數
          const rank = 10000000;
    
          // 生成同一份測試數據的輔助代碼
          const random = Math.random;
          const array = new Array(openCount);
    
          // 生成同一份測試數據
          for (var i = 0; i < openCount; i++)
             array[i] = Math.floor(random() * rank);
    
          // 建立AVL樹實例
          const avl = new MyAVLTree();
    
          for (const value of array) avl.add(value);
    
          // 輸出當前這棵avl樹是不是一個二分搜索樹
          this.show('Is Binary Search Tree : ' + avl.isBanarySearchTree());
          console.log('Is Binary Search Tree : ' + avl.isBanarySearchTree());
    
          // 輸出當前這棵avl樹是不是一個平衡二叉樹
          this.show('Is Balanced : ' + avl.isBalanced());
          console.log('Is Balanced : ' + avl.isBalanced());
       }
    
       // 將內容顯示在頁面上
       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();
    };
    複製代碼

AVL 樹 旋轉基本原理

  1. AVL 樹是經過兩個主要的機制來實現自平衡的

    1. 左旋轉和右旋轉
  2. AVL 樹維護自平衡在何時發生

    1. 在二分搜索樹中若是想要插入一個節點,
    2. 必須從根節點開始一路尋找到這個節點正確的插入位置,
    3. 只有在你新添加一個節點纔有可能致使整棵二分搜索樹再也不知足平衡性,
    4. 相應的這個不平衡的節點只有可能發生在插入的這個位置一直到其父祖節點這些節點中,
    5. 這是由於是插入了這個節點以後才破壞了整棵樹的平衡性,
    6. 破環的這個平衡性會反應到這個節點相應的父親節點或者祖先節點上,
    7. 由於新插入了這個節點以後,
    8. 它的父親節點或者祖先節點相應的左右子樹的高度值就須要進行更新,
    9. 在更新以後,有可能平衡因子就會大於1或者小於-1
    10. 也就是左右子樹的高度差超過了一,
    11. 因此維護平衡的時機應該是加入節點以後,沿着這個節點向上去回溯來維護這個平衡性,
    12. 由於這個總體代碼是一個遞歸代碼,
    13. 因此沿着這個節點向上維護平衡性自己也是很是容易的,
    14. 在 add 方法中顯示遞歸到底的代碼塊兒,而後是選擇插入節點的位置進行遞歸調用,
    15. 最後是更新節點的高度以及計算平衡因子,
    16. 計算出平衡因子以後就能夠開始維護這個平衡性等一些特殊的操做,
    17. 維護完平衡性以後對應的 node 進行返回,返回給遞歸調用的上一層,
    18. 這就是一個回溯的過程,對每個節點都維護一下平衡性,
    19. 維護完平衡性以後,到遞歸的上一層,也就是看當前處理的這個節點相應的它的父親節點,
    20. 在遞歸的上一層若是發現它的平衡因子依然不知足要求,也就是大於一或者小於負一,
    21. 而後再在這裏維護平衡性,維護之後再將相應的根節點返回給上一層,依次類推,
    22. 這裏就是維護二分搜索樹的平衡性相應的時機所在的這個位置,
    23. 可使用宏觀的角度去理解一下整個添加節點的遞歸函數在作什麼,
    24. 而後能夠再從微觀的角度可使用一些相對小的數據集來看一下這個具體的執行過程。
  3. AVL 樹維護平衡性的原理

    1. 假如當前這棵樹爲空,在這種狀況下先添加一個節點,這個節點叫節點 12,
    2. 此時對這個節點來講,它的平衡因子就是 0,
    3. 在這個節點的基礎上再添加一個元素,這個節點叫作節點 8,
    4. 由於節點 8 比節點 12 小,因此就在這個節點 12 的左子樹上,
    5. 如今節點 8 的平衡因子就變成了 0,相應的節點 12 這個節點它對應的平衡因子就變成了 1。
    6. 這個平衡因子更新的過程就是在模擬程序執行的過程,
    7. 添加這個節點 8 的時候在節點 12 的左子樹中添加這個新的節點,
    8. 一個新的節點,它的平衡因子默認就爲 0,以後因爲新添加了一個節點,
    9. 這個新的節點它的祖輩節點相應的平衡因子就會發生改變,
    10. 因此就回到了節點 12 更新了它的平衡因子,因此節點 12 的平衡因子變成了 1。
    11. 而後再在這棵樹中添加一個節點 5,因爲 5 比 8 要小,
    12. 因此它會一路下來最終成爲節點 8 的左子樹,此時節點 5 是一個葉子節點,
    13. 它的平衡因子爲 0,那麼回到父節點 8 這裏,因爲節點 8 的左子樹多了一個節點,
    14. 它的平衡因子就變成了 1,那麼在回到節點 12 這裏,節點 12 的平衡因子相應的也要更新,
    15. 此時就變爲了 2,那麼在節點 12 上,它的平衡因子的絕對值已經大於了 1,
    16. 因此就須要對它進行一個平衡的維護,
    17. 那麼這就是對於一個節點它的平衡性被打破的一個最通常的狀況,
    18. 也就是添加節點是不停的向一側添加,造成了一條鏈表的形狀,
    19. 顯然它會不平衡,這樣的一種狀況不只僅可能出如今初始的狀況,
    20. 還有可能出如今對一個非空的樹進行節點添加的狀況,
    21. 其實都是插入的元素在不平衡的節點的左側的左側,
    22. 換句話說,就是一直在向整棵樹的左側添加元素,最終致使父祖輩節點的平衡因子大於一,
    23. 也就是左子樹的高度比右子樹的高度要高,並且高出來的幅度是比一還要大的,
    24. 與此同時這個不平衡節點的左子樹它的平衡因子也是大於 0 的,
    25. 換句話來講,對於這個不平衡節點的左子樹的高度也是大於右子樹的,
    26. 也就是插入的元素是在最終造成不平衡節點的左側的左側,
    27. 那麼它就會知足不符合 AVL 樹性質的樣子,此時就能夠進行平衡的維護。
    28. 添加每個節點以後,就會更新這個節點的高度值,同時計算出這個節點的平衡因子,
    29. 這個平衡因子就有可能打破平衡二叉樹的條件,也就是平衡因子的絕對值大於一,
    30. 因此就須要在下面進行平衡的維護。
  4. 右旋轉操做

    1. 在上面分析的狀況中,若是這個節點的平衡因子比一還要大,
    2. 也就是左子樹比右子樹的高度差是超過了一的,而且是左邊要高的,
    3. 與此同時再來看一下當前這個節點的左子樹的平衡因子,
    4. 若是左子樹的平衡因子對應的是大於等於 0 的,
    5. 在這種狀況下也就是說明了當前這個節點不平衡的緣由,
    6. 是在於它的左側的左側多添加了一個節點,因此要針對這種狀況進行一個平衡的維護,
    7. 那麼這種平衡的維護操做叫作右旋轉,
    8. 先取出當前節點的左子樹的右子樹進行保存,
    9. 而後讓當前節點變成當前節點的左子樹的右子樹,
    10. 最後讓當前節點的左子樹變成以前保存的右子樹,
    11. 那麼最開始的當前節點左子樹就變成了新的二叉樹的根節點,
    12. 這樣的一個過程就叫作右旋轉,
    13. 這個過程其實至關於最開始的當前節點順時針的轉到了
    14. 當前節點的左子樹的右子樹的位置,
    15. 從當前節點的左子樹的角度看就是當前節點順時針的向右進行了一個旋轉,
    16. 此時獲得的這棵新的二叉樹即知足二分搜索樹的性質,
    17. 又知足平衡二叉樹的性質,以下圖,承載的元素是一致的。
    // 最開始這棵樹是這種狀況 T1 < Z < T2 < X < T3 < Y < T4
    // (Y)
    // / \
    // (X) (T4)
    // / \
    // (Z) (T3)
    // / \
    // (T1) (T2)
    
    // 右旋轉後是這樣子,依然是T1 < Z < T2 < X < T3 < Y < T4
    // (X)
    // / \
    // (Z) (Y)
    // / \ / \
    // (T1) (T2)(T3)(T4)
    複製代碼

代碼示例

  1. AVLTree
// 自定義AVL樹節點 AVLTreeNode
class MyAVLTreeNode {
   constructor(key = null, value = null, left = null, right = null) {
      this.key = key;
      this.value = value;
      this.left = left;
      this.right = right;
      this.height = 1;
   }

   // @Override toString 2018-11-24-jwl
   toString() {
      return this.key + '--->' + this.value + '--->' + this.height;
   }
}

// 自定義AVL樹 AVLTree
class MyAVLTree {
   constructor() {
      this.root = null;
      this.size = 0;
   }

   // 比較的功能
   compare(keyA, keyB) {
      if (keyA === null || keyB === null)
         throw new Error("key is error. key can't compare.");
      if (keyA > keyB) return 1;
      else if (keyA < keyB) return -1;
      else return 0;
   }

   // 獲取某個節點的高度 -
   getHeight(node) {
      // 節點爲空 返回0
      if (!node) return 0;

      // 直接返回這個節點的高度
      return node.height;
   }

   // 獲取一個節點的平衡因子 -
   getBalanceFactor(node) {
      // 節點爲空 返回0
      if (!node) return 0;

      // 左右子樹的高度值
      const leftHeight = this.getHeight(node.left);
      const rightHeight = this.getHeight(node.right);

      // 左子樹的高度 - 右子樹高度的值 = 平衡因子
      return leftHeight - rightHeight;
   }

   // 根據key獲取節點 -
   getNode(node, key) {
      // 先解決最基本的問題
      if (node === null) return null;

      // 開始將複雜的問題 逐漸縮小規模
      // 從而求出小問題的解,最後構建出原問題的解
      switch (this.compare(node.key, key)) {
         case 1: // 向左找
            return this.getNode(node.left, key);
            break;
         case -1: // 向右找
            return this.getNode(node.right, key);
            break;
         case 0: // 找到了
            return node;
            break;
         default:
            throw new Error(
               'compare result is error. compare result : 0、 一、 -1 .'
            );
            break;
      }
   }

   // 添加操做 +
   add(key, value) {
      this.root = this.recursiveAdd(this.root, key, value);
   }

   // 添加操做 遞歸算法 -
   recursiveAdd(node, key, value) {
      // 解決最簡單的問題
      if (node === null) {
         this.size++;
         return new MyAVLTreeNode(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;

      // 在這裏對節點的高度進行從新計算 節點自己高度爲1
      // 計算方式: 1 + 左右子樹的height值最大的那個height值
      node.height =
         1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));

      // 計算一個節點的平衡因子
      const balanceFactor = this.getBalanceFactor(node);

      // 平衡維護 右旋轉操做 平衡因子爲正數則表示左傾 反之爲右傾
      if (balanceFactor > 1 && this.getBalanceFactor(node.left) >= 0) {
      }

      // // 若是平衡因子的絕對值大於1 說明不知足AVL平衡二叉樹的性質了
      // if (Math.abs(balanceFactor) > 1) {
      // console.log(node.toString() + " unbalanced : " + balanceFactor + "\r\n");
      // document.body.innerHTML += node.toString() + " unbalanced : " + balanceFactor + "<br/>";
      // }

      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;
   }

   // 判斷當前這棵樹是不是一棵二分搜索樹,有二分搜索樹順序性
   isBanarySearchTree() {
      // 若是節點爲空 那麼這就是一棵空的二分搜索樹
      if (!this.root) return true;

      // 存儲二分搜索樹中的key
      const list = new Array();

      // 中序遍歷後,添加到list中的值會是以從小到大升序的樣子排列
      this.inOrder(this.root, list);

      // 從前日後判斷 list中的值是不是從小到大升序的排列
      // 驗證 當前樹是否符合二分搜索樹的性質
      for (var i = 1; i < list.length; i++)
         if (list[i - 1] > list[i]) return false;
      return true;
   }

   // 中序遍歷 輔助函數 -
   inOrder(node, list) {
      // 遞歸到底的狀況
      if (!node) return;

      // 中序遍歷時,添加到數組中的值會是以從小到大升序的樣子排列
      this.inOrder(node.left, list);
      list.push(node.key);
      this.inOrder(node.right, list);
   }

   // 判斷該二叉樹是否一棵平衡二叉樹
   isBalanced() {
      return this.recursiveIsBalanced(this.root);
   }

   // 遞歸判斷某一個節點是否符合平衡二叉樹的定義 輔助函數 -
   recursiveIsBalanced(node) {
      // 可以遞歸到底,說明符合要求
      // 空的節點左右孩子高度差確定爲0,
      // 由於空樹沒有左右子樹,更加談不上下面去判斷它的左右子樹高度差是否會超過一。
      if (!node) return true;

      // 若是當前節點的高度差大於1 說明不符合要求
      if (Math.abs(this.getBalanceFactor(node)) > 1) return false;

      // 遞歸的去判斷當前節點的 左右子樹是否符合要求
      return (
         this.recursiveIsBalanced(node.left) &&
         this.recursiveIsBalanced(node.right)
      );
   }

   // @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. Main
// main 函數
class Main {
   constructor() {
      this.alterLine('AVLTree Area');
      // 千級別
      const openCount = 100; // 操做數
      const rank = 10000000;

      // 生成同一份測試數據的輔助代碼
      const random = Math.random;
      const array = new Array(openCount);

      // 生成同一份測試數據
      for (var i = 0; i < openCount; i++)
         array[i] = Math.floor(random() * rank);

      // 建立AVL樹實例
      const avl = new MyAVLTree();

      for (const value of array) avl.add(value);

      // 輸出當前這棵avl樹是不是一個二分搜索樹
      this.show('Is Binary Search Tree : ' + avl.isBanarySearchTree());
      console.log('Is Binary Search Tree : ' + avl.isBanarySearchTree());

      // 輸出當前這棵avl樹是不是一個平衡二叉樹
      this.show('Is Balanced : ' + avl.isBalanced());
      console.log('Is Balanced : ' + avl.isBalanced());
   }

   // 將內容顯示在頁面上
   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();
};
複製代碼

AVL 樹 左旋轉和右旋轉的實現

  1. 右旋轉

    1. 先取出當前節點的左子樹的右子樹進行保存,
    2. 而後讓當前節點變成當前節點的左子樹的右子樹,
    3. 最後讓當前節點的左子樹變成以前保存的右子樹,
    4. 那麼最開始的當前節點左子樹就變成了新的二叉樹的根節點,
    5. 這樣的一個過程就叫作右旋轉。
    6. 右旋轉是當前節點的平衡因子大於正一而且
    7. 左子樹的平衡因子大於等於 0 的時候才進行的特殊操做,
    8. 右旋轉以後要更新一下對應節點的高度值,只須要更新 x 和 y 的高度值便可,
    9. 從新計算一下 x 和 y 的左右子樹的最大高度值而後+1 便可。
      // 對節點y進行向右旋轉操做,返回旋轉後新的根節點x
      // y x
      // / \ / \
      // x T4 向右旋轉 (y) z y
      // / \ - - - - - - - -> / \ / \
      // z T3 T1 T2 T3 T4
      // / \
      // T1 T2
      複製代碼
  2. 左旋轉

    1. 與右旋轉對應的是左旋轉,
    2. 右旋轉是插入的元素在不平衡的節點的左側的左側,
    3. 而左旋轉是插入的元素在不平衡的節點的右側的右側,
    4. 左旋轉和右旋轉是一個左右對稱的狀況,
    5. 也就是當前節點的右子樹的高度值比左子樹的高度值相差大於一,
    6. 換句話說就是左子樹的高度值減去右子樹的高度值小於了負一,
    7. 在這種狀況下就須要進行左旋轉,
    8. 先將當前節點的右子樹的左子樹保存起來,
    9. 而後讓當前節點的右子樹的左子樹等於當前節點,
    10. 最後讓當前節點的右子樹等於以前保存的左子樹,
    11. 這個過程就是左旋轉,最終獲得的二叉樹同樣即知足而二分搜索樹的性質,
    12. 同時也知足了平衡二叉樹的性質。
    13. 左旋轉和右旋轉是一個徹底對稱的關係。
    14. 左旋轉是 當前節點的平衡因子小於負一而且
    15. 右子樹的平衡因子小於等於 0 的時候才進行的特殊操做,
    16. 也就是整棵樹總體上是向右進行傾斜的,
    17. 左旋轉以後要更新一下對應節點的高度值,只須要更新 x 和 y 的高度值便可,
    18. 從新計算一下 x 和 y 的左右子樹的最大高度值而後+1 便可。
    // 最開始這棵樹是這種狀況 T4 < Y < T3 < X < T1 < Z < T2
    // (Y)
    // / \
    // (T4) (X)
    // / \
    // (T3) (Z)
    // / \
    // (T1) (T2)
    
    // 左旋轉後是這樣子,依然是T4 < Y < T3 < X < T1 < Z < T2
    // (X)
    // / \
    // (Y) (Z)
    // / \ / \
    // (T4)(T3)(T1)(T2)
    複製代碼
  3. 不管是左旋轉仍是右旋轉都是一種狀況

    1. 就是以當前節點爲根的向一側偏斜,
    2. 不過是向左偏斜仍是向右偏斜對應了是使用左旋轉仍是右旋轉,
    3. 向左偏斜就向右旋轉,向右偏斜就向左旋轉。
  4. 使用了左旋轉和右旋轉來進行平衡性的維護以後

    1. 其實還有另外兩種狀況須要考慮,
    2. 只有這樣纔可以將全部的節點的平衡因子的絕對值小於等於一。

代碼示例

  1. AVLTree

    // 自定義AVL樹節點 AVLTreeNode
    class MyAVLTreeNode {
       constructor(key = null, value = null, left = null, right = null) {
          this.key = key;
          this.value = value;
          this.left = left;
          this.right = right;
          this.height = 1;
       }
    
       // @Override toString 2018-11-24-jwl
       toString() {
          return this.key + '--->' + this.value + '--->' + this.height;
       }
    }
    
    // 自定義AVL樹 AVLTree
    class MyAVLTree {
       constructor() {
          this.root = null;
          this.size = 0;
       }
    
       // 比較的功能
       compare(keyA, keyB) {
          if (keyA === null || keyB === null)
             throw new Error("key is error. key can't compare.");
          if (keyA > keyB) return 1;
          else if (keyA < keyB) return -1;
          else return 0;
       }
    
       // 獲取某個節點的高度 -
       getHeight(node) {
          // 節點爲空 返回0
          if (!node) return 0;
    
          // 直接返回這個節點的高度
          return node.height;
       }
    
       // 獲取一個節點的平衡因子 -
       getBalanceFactor(node) {
          // 節點爲空 返回0
          if (!node) return 0;
    
          // 左右子樹的高度值
          const leftHeight = this.getHeight(node.left);
          const rightHeight = this.getHeight(node.right);
    
          // 左子樹的高度 - 右子樹高度的值 = 平衡因子
          return leftHeight - rightHeight;
       }
    
       // 根據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;
          }
       }
    
       // 對節點y進行向右旋轉操做,返回旋轉後新的根節點x
       // y x
       // / \ / \
       // x T4 向右旋轉 (y) z y
       // / \ - - - - - - - -> / \ / \
       // z T3 T1 T2 T3 T4
       // / \
       // T1 T2
       rightRotate(y) {
          const x = y.left;
          const T3 = x.right;
    
          // 向右旋轉的過程
          y.left = T3;
          x.right = y;
    
          // 更新節點的height值 只須要更新x和y的便可
          y.height =
             1 + Math.max(this.getHeight(y.left), this.getHeight(y.right));
          x.height =
             1 + Math.max(this.getHeight(x.left), this.getHeight(x.right));
    
          // 返回 新節點 x
          return x;
       }
    
       // 對節點y進行向左旋轉操做,返回旋轉後新的根節點x
       // y x
       // / \ / \
       // T1 x 向左旋轉 (y) y z
       // / \ - - - - - - - -> / \ / \
       // T2 z T1 T2 T3 T4
       // / \
       // T3 T4
       leftRotate(y) {
          const x = y.right;
          const T2 = x.left;
    
          // 向左旋轉的過程
          y.right = T2;
          x.left = y;
    
          // 更新節點的height值 只須要更新x和y的便可
          y.height =
             1 + Math.max(this.getHeight(y.left), this.getHeight(y.right));
          x.height =
             1 + Math.max(this.getHeight(x.left), this.getHeight(x.right));
    
          // 返回 新節點 x
          return x;
       }
    
       // 添加操做 +
       add(key, value) {
          this.root = this.recursiveAdd(this.root, key, value);
       }
    
       // 添加操做 遞歸算法 -
       recursiveAdd(node, key, value) {
          // 解決最簡單的問題
          if (node === null) {
             this.size++;
             return new MyAVLTreeNode(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;
    
          // 在這裏對節點的高度進行從新計算 節點自己高度爲1
          // 計算方式: 1 + 左右子樹的height值最大的那個height值
          node.height =
             1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
    
          // 計算一個節點的平衡因子
          const balanceFactor = this.getBalanceFactor(node);
    
          // 若是平衡因子的絕對值大於1 說明不知足AVL平衡二叉樹的性質了
          if (Math.abs(balanceFactor) > 1) {
             console.log(
                node.toString() + ' unbalanced : ' + balanceFactor + '\r\n'
             );
             document.body.innerHTML +=
                node.toString() + ' unbalanced : ' + balanceFactor + '<br/>';
          }
    
          // 平衡維護 右旋轉操做 平衡因子爲正數則表示左傾 反之爲右傾
          if (balanceFactor > 1 && this.getBalanceFactor(node.left) >= 0)
             return this.rightRotate(node);
    
          // 平衡維護 右旋轉操做 平衡因子爲負數則表示右傾 反之爲左傾
          if (balanceFactor < -1 && this.getBalanceFactor(node.left) <= 0)
             return this.leftRotate(node);
    
          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;
       }
    
       // 判斷當前這棵樹是不是一棵二分搜索樹,有二分搜索樹順序性
       isBanarySearchTree() {
          // 若是節點爲空 那麼這就是一棵空的二分搜索樹
          if (!this.root) return true;
    
          // 存儲二分搜索樹中的key
          const list = new Array();
    
          // 中序遍歷後,添加到list中的值會是以從小到大升序的樣子排列
          this.inOrder(this.root, list);
    
          // 從前日後判斷 list中的值是不是從小到大升序的排列
          // 驗證 當前樹是否符合二分搜索樹的性質
          for (var i = 1; i < list.length; i++)
             if (list[i - 1] > list[i]) return false;
          return true;
       }
    
       // 中序遍歷 輔助函數 -
       inOrder(node, list) {
          // 遞歸到底的狀況
          if (!node) return;
    
          // 中序遍歷時,添加到數組中的值會是以從小到大升序的樣子排列
          this.inOrder(node.left, list);
          list.push(node.key);
          this.inOrder(node.right, list);
       }
    
       // 判斷該二叉樹是否一棵平衡二叉樹
       isBalanced() {
          return this.recursiveIsBalanced(this.root);
       }
    
       // 遞歸判斷某一個節點是否符合平衡二叉樹的定義 輔助函數 -
       recursiveIsBalanced(node) {
          // 可以遞歸到底,說明符合要求
          // 空的節點左右孩子高度差確定爲0,
          // 由於空樹沒有左右子樹,更加談不上下面去判斷它的左右子樹高度差是否會超過一。
          if (!node) return true;
    
          // 若是當前節點的高度差大於1 說明不符合要求
          if (Math.abs(this.getBalanceFactor(node)) > 1) return false;
    
          // 遞歸的去判斷當前節點的 左右子樹是否符合要求
          return (
             this.recursiveIsBalanced(node.left) &&
             this.recursiveIsBalanced(node.right)
          );
       }
    
       // @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;
       }
    }
    複製代碼
  2. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('AVLTree Area');
          // 千級別
          const openCount = 100; // 操做數
          const rank = 10000000;
    
          // 生成同一份測試數據的輔助代碼
          const random = Math.random;
          const array = new Array(openCount);
    
          // 生成同一份測試數據
          for (var i = 0; i < openCount; i++)
             array[i] = Math.floor(random() * rank);
    
          // 建立AVL樹實例
          const avl = new MyAVLTree();
    
          for (const value of array) avl.add(value);
    
          // 輸出當前這棵avl樹是不是一個二分搜索樹
          this.show('Is Binary Search Tree : ' + avl.isBanarySearchTree());
          console.log('Is Binary Search Tree : ' + avl.isBanarySearchTree());
    
          // 輸出當前這棵avl樹是不是一個平衡二叉樹
          this.show('Is Balanced : ' + avl.isBalanced());
          console.log('Is Balanced : ' + avl.isBalanced());
       }
    
       // 將內容顯示在頁面上
       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();
    };
    複製代碼
相關文章
相關標籤/搜索