數據結構(一)-- 平衡樹
文章是對鄧俊輝老師數據結構教程的總結,部分圖片資料來自鄧俊輝老師的教學PPT
建議閱讀前先閱讀參考文章的第二,三文章,總結得非常好!
文章部分代碼和圖片來自參考文章的第二,三文章!!
閱讀前提幾個問題吧 ,幫助思考
- 為什麽需要平衡二叉樹
- AVL 需要兩次旋轉的操作為什麽不直接分解為左旋和右旋,還要LR RL 呢
- AVL 有什麽局限性
二叉查找樹 (Binary Search Tree -- BST)
若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
從思路上講我們希望二叉查找樹可以結合向量和鏈表的結構,但是在某些情況下,時間復雜度還是沒能達到要求。
像上面的情況講,最壞的情況取決於樹的高度,當生成像圖中的結構時,二叉查找樹就成了單鏈表,查找效率降至O(n),查找時自然是從第一個查找到最後一個,這樣的時間復雜度是無法讓人接受的。
理想平衡
既然二叉搜索樹性能取決於其高度,那我們自然想到盡可能降低其高度,使兄弟子樹的高度彼此接近。
由於包含n個節點的二叉樹,高度不可能小於[log2n],若恰好高為[log2n],則稱為理想平衡樹,如完全二叉樹(CBT)、滿二叉樹均屬此類。
然而,葉節點只能出現於最底兩層的限制過於苛刻,此類二叉樹所占比例極低,從算法可行性角度來看,應當依照某種相對寬松的標準,重新定義。即是說我們可以約定一定的條件使得某棵樹可以接近或是等同於理想平衡樹,我們就可以達到目的了,就是下圖中的平衡二叉搜索樹(Balance BST---BBST)。
至此我們知道了有這麽幾種樹 :
- 二叉搜索樹 BST
- 完全二叉樹 CBT
- 平衡二叉搜索樹 BBST
下圖解釋了理想平衡(理想目標)和適度平衡(追求的目標)
等價BST
上面這張圖可以看到兩個平衡樹,中序遍歷是相同的(樹下面的數字),但是撲拓結構是不同的,兩者是等價的,他們之間的特點是 : 上下可變,左右不亂。(這個非常重要,後面為使樹適度平衡實際上就是在依據這兩個特點來進行的!!)
兩者之間的變換可以通過以下的方式 :
可以看到 zig 是順時針旋轉,而 zag 是逆時針旋轉,目的是使二叉樹平衡。最多操作的次數不要超過 Logn
我們後面講的AVL ,remove 方法最壞的情況時間復雜度是 Logn ,所以後面的紅黑樹等會繼續對他改良。
關於的BST 的API方法可以看這一篇文章 : http://www.cnblogs.com/penghuwan/p/8057482.html#_label10
AVL (Adelson-Velskii and Landis)
下面部分代碼和圖片來自參考資料
講 AVL 樹之前我們先來看看 CBT BBST BST 之間的關系,我們構建一棵適度平衡的樹最重要要解決的兩個問題就是 :
- 如何界定BBST
- rebalance 的邏輯實現
後面我們會接觸到各種適度平衡的樹就是圍繞這兩個方面展開的,回到圖中,其中 AVL 就是紫色點(有點小,認真看),存在於BBST中,當失衡的時候跑到了BBST之外,通過rebalance 操作重新回到了 BBST 之中。
下面開始介紹AVL 。AVL 是名字的縮寫,是發明這個數據結構的人。學習AVL 這個數據結構之前,我們先對二叉堆高度進行定義
接下來是AVL的定義 :(來自維基百科)
In a binary tree the balance factor of a node N is defined to be the height difference
- BalanceFactor(N) := Height(RightSubtree(N)) – Height(LeftSubtree(N)) [6]
of its two child subtrees. A binary tree is defined to be an AVL tree if the invariant
- BalanceFactor(N) ∈ {–1,0,+1}[7]
holds for every node N in the tree.
平衡因子 = 左子樹節點高度 - 右子樹節點高度
平衡二叉樹(AVL): 所有結點的平衡因子的絕對值都不超過1。即對平衡二叉樹每個結點來說,其左子樹的高度 - 右子樹高度得到的差值只能為 1, 0 , -1 這三個值。 取得小於 -1或者大於1的值,都被視為打破了二叉樹的平衡。
例如下圖
為了使樹平衡,使用的手段有 : 左旋和右旋。
右旋(左旋一樣的)
左旋,即是逆時針旋轉;右旋, 即是順時針旋轉。
下面是最簡單的右旋(下面代碼和圖片出處)
當然還有這一種情況,其中數字4代表的節點可有可無,無的情況為NULL
下面是旋轉的情況
代碼應該是
1 /** 2 * @description: 右旋方法 3 */ 4 private Node rotateRight (Node x) { 5 Node y = x.left; // 取得x的左兒子 6 x.left = y.right; // 將x左兒子的右兒子("拖油瓶"結點)鏈接到旋轉後的x的左鏈接中 7 y.right = x; // 調轉x和它左兒子的父子關系,使x成為它原左兒子的右子樹 8 x.height = max(height(x.left),height(x.right)) + 1; // 更新並維護受影響結點的height 9 y.height = max(height(y.left),height(y.right)) + 1; // 更新並維護受影響結點的height 10 return y; // 將y返回 11 }
其中x為失衡點。而左旋的分析和右旋的情況是一樣的。但是一個失衡點有可能是包含了左旋後右旋,或是右旋後左旋。所有下面羅列一下使樹平衡會遇到的情況。
四種情況
下面總結來自參考資料
1. 單次右旋: 由於在a的左子樹的根結點的左子樹上插入結點(LL),使a的平衡因子由1變成2, 導致以a為根的子樹失去平衡, 則需進行一次的向右的順時針旋轉操作
2. 單次左旋: 由於在a的右子樹根結點的右子樹上插入結點(RR),a的平衡因子由-1變成-2,導致以a為根結點的子樹失去平衡,則需要進行一次向左的逆時針旋轉操作
3. 兩次旋轉、先左旋後右旋: 由於在a的左子樹根結點的右子樹上插入結點(LR), 導致a的平衡因子由1變成2,導致以a為根結點的子樹失去平衡,需要進行兩次旋轉, 先左旋後右旋
4.兩次旋轉, 先右旋後左旋: 由於在a的右子樹根結點的左子樹上插入結點(RL), a的平衡因子由-1變成-2,導致以a為根結點的子樹失去平衡, 則需要進行兩次旋轉,先右旋後左旋
那麽問題來了,怎麽分別判斷LL, RR,LR,RL這四種破環平衡的場景呢?
我們可以根據當前破壞平衡的結點的平衡因子, 以及其孩子結點的平衡因子來判斷,具體如下圖所示:
(BF表示平衡因子, 最下方的那個結點是新插入的結點)
插入和刪除
代碼出處見參考資料,非原創
先放出完整代碼
1 package Avl; 2 3 import java.util.LinkedList; 4 5 /** 6 * @Author: HuWan Peng 7 * @Date Created in 10:35 2017/12/29 8 */ 9 public class AVL { 10 Node root; // 根結點 11 12 private class Node { 13 int key, val; 14 Node left, right; 15 int height = 1; // 每個結點的高度屬性 16 17 public Node(int key, int val) { 18 this.key = key; 19 this.val = val; 20 } 21 } 22 23 /** 24 * @description: 返回兩個數中的最大值 25 */ 26 private int max(int a, int b) { 27 return a > b ? a : b; 28 } 29 30 /** 31 * @description: 獲得當前結點的高度 32 */ 33 private int height(Node x) { 34 if (x == null) 35 return 0; 36 return x.height; 37 } 38 39 /** 40 * @description: 獲得平衡因 41 */ 42 private int getBalance(Node x) { 43 if (x == null) 44 return 0; 45 return height(x.left) - height(x.right); 46 } 47 48 /** 49 * @description: 右旋方法 50 */ 51 private Node rotateRight(Node x) { 52 Node y = x.left; // 取得x的左兒子 53 x.left = y.right; // 將x左兒子的右兒子("拖油瓶"結點)鏈接到旋轉後的x的左鏈接中 54 y.right = x; // 調轉x和它左兒子的父子關系,使x成為它原左兒子的右子樹 55 x.height = max(height(x.left), height(x.right)) + 1; // 更新並維護受影響結點 56 y.height = max(height(y.left), height(y.right)) + 1; // 更新並維護受影響結點 57 return y; // 將y返回 58 } 59 60 /** 61 * @description: 左旋方法 62 */ 63 private Node rotateLeft(Node x) { 64 Node y = x.right; // 取得x的右兒子 65 x.right = y.left; // 將x右兒子的左兒子("拖油瓶"結點)鏈接到旋轉後的x的右鏈接中 66 y.left = x; // 調轉x和它右兒子的父子關系,使x成為它原右兒子的左子樹 67 x.height = max(height(x.left), height(x.right)) + 1; // 更新並維護受影響結點 68 y.height = max(height(y.left), height(y.right)) + 1; // 更新並維護受影響結點 69 return y; // 將y返回 70 } 71 72 /** 73 * @description: 平衡 操作 74 */ 75 private Node reBalance(Node x) { 76 int balanceFactor = getBalance(x); 77 if (balanceFactor > 1 && getBalance(x.left) > 0) { // LL型,進行單次右旋 78 return rotateRight(x); 79 } 80 if (balanceFactor > 1 && getBalance(x.left) <= 0) { // LR型 先左旋再右旋 81 Node t = rotateLeft(x); 82 return rotateRight(t); 83 } 84 if (balanceFactor < -1 && getBalance(x.right) <= 0) {// RR型, 進行單次左旋 85 return rotateLeft(x); 86 } 87 if (balanceFactor < -1 && getBalance(x.right) > 0) {// RL型,先右旋再左旋 88 Node t = rotateRight(x); 89 return rotateLeft(t); 90 } 91 return x; 92 } 93 94 /** 95 * @description: 插入結點(鍵值對) 96 */ 97 public Node put(Node x, int key, int val) { 98 if (x == null) 99 return new Node(key, val); // 插入鍵值對 100 if (key < x.key) 101 x.left = put(x.left, key, val); // 向左子樹遞歸插入 102 else if (key > x.key) 103 x.right = put(x.right, key, val); // 向右子樹遞歸插入 104 else 105 x.val = val; // key已存在, 替換val 106 107 x.height = max(height(x.left), height(x.right)) + 1; // 沿遞歸路徑從下至上更新結點height屬性 108 x = reBalance(x); // 沿遞歸路徑從下往上, 檢測當前結點是否失衡,若失衡則進行平衡化 109 return x; 110 } 111 112 public void put(int key, int val) { 113 root = put(root, key, val); 114 } 115 116 /** 117 * @description: 返回最小鍵 118 */ 119 private Node min(Node x) { 120 if (x.left == null) 121 return x; // 如果左兒子為空,則當前結點鍵為最小值,返回 122 return min(x.left); // 如果左兒子不為空,則繼續向左遞歸 123 } 124 125 public int min() { 126 if (root == null) 127 return -1; 128 return min(root).key; 129 } 130 131 /** 132 * @description: 刪除最小鍵的結點 133 */ 134 public Node deleteMin(Node x) { 135 if (x.left == null) 136 return x.right; // 如果當前結點左兒子空,則將右兒子返回給上一層遞歸的x.left 137 x.left = deleteMin(x.left);// 向左子樹遞歸, 同時重置搜索路徑上每個父結點指向左兒子的鏈接 138 return x; // 當前結點不是min 139 } 140 141 public void deleteMin() { 142 root = deleteMin(root); 143 } 144 145 /** 146 * @description: 刪除給定key的鍵值對 147 */ 148 private Node delete(int key, Node x) { 149 if (x == null) 150 return null; 151 if (key < x.key) 152 x.left = delete(key, x.left); // 向左子樹查找鍵為key的結點 153 else if (key > x.key) 154 x.right = delete(key, x.right); // 向右子樹查找鍵為key的結點 155 else { 156 // 結點已經被找到,就是當前的x 157 if (x.left == null) 158 return x.right; // 如果左子樹為空,則將右子樹賦給父節點的鏈接 159 if (x.right == null) 160 return x.left; // 如果右子樹為空,則將左子樹賦給父節點的鏈接 161 Node inherit = min(x.right); // 取得結點x的繼承結點 162 inherit.right = deleteMin(x.right); // 將繼承結點從原來位置刪除,並重置繼承結點右鏈接 163 inherit.left = x.left; // 重置繼承結點左鏈接 164 x = inherit; // 將x替換為繼承結點 165 } 166 if (root == null) 167 return root; 168 x.height = max(height(x.left), height(x.right)) + 1; // 沿遞歸路徑從下至上更新結點height屬性 169 x = reBalance(x); // 沿遞歸路徑從下往上, 檢測當前結點是否失衡,若失衡則進行平衡化 170 return x; 171 } 172 173 public void delete(int key) { 174 root = delete(key, root); 175 } 176 177 178 /** 179 * 二叉樹層序遍歷 180 */ 181 private void levelIterator() { 182 LinkedList<Node> queue = new LinkedList<Node>(); 183 Node current = null; 184 int childSize = 0; 185 int parentSize = 1; 186 queue.offer(root); 187 while (!queue.isEmpty()) { 188 current = queue.poll();// 出隊隊頭元素並訪問 189 System.out.print(current.val + "-->"); 190 if (current.left != null)// 如果當前節點的左節點不為空入隊 191 { 192 queue.offer(current.left); 193 childSize++; 194 } 195 if (current.right != null)// 如果當前節點的右節點不為空,把右節點入隊 196 { 197 queue.offer(current.right); 198 childSize++; 199 } 200 parentSize--; 201 if (parentSize == 0) { 202 parentSize = childSize; 203 childSize = 0; 204 System.out.println(""); 205 } 206 } 207 } 208 209 public void printOutMidNums(){ 210 211 } 212 213 public static void main(String[] args) { 214 AVL avl = new AVL(); 215 avl.put(1, 11); 216 avl.put(2, 22); 217 avl.put(3, 33); 218 avl.put(4, 44); 219 avl.put(5, 55); 220 avl.put(6, 66); 221 avl.levelIterator(); 222 } 223 }
插入操作的代碼,需要註意的是rotateLeft 或是 rotateRight 方法內部是沒有連接上一個父節點的操作的,重連父節點的操作在遞歸中。
刪除操作的解釋見 http://www.cnblogs.com/penghuwan/p/8057482.html#_label10,下面引用這篇文章的話來解釋刪除操作。
首先介紹 繼承結點,繼承結點就是某個結點被刪除後,能夠“繼承”某個結點的結點,下面的圖片是繼承結點的定義
它的作用,用一個例子來說明一下。
相對於14,15是它的繼承結點,假若14 被15替換掉,16成為18的左子樹節點。那麽仍然能保持整顆二叉查找樹的有序性。
下面說一下刪除的三種情況
其中第三種就是使用繼承結點的情況。其中需要註意的是,
有可能刪除操作後,子樹的高度 –1 ,而連接子樹的上層父節點或是祖父節點因為子樹的高度 –1 ,導致繼續 rebalance,所以刪除操作的最壞情況可以達到 Logn
AVL 綜合評價
補充
這裏推薦一個網站,動態算法的網站 :
https://www.cs.usfca.edu/~galles/visualization/AVLtree.html
參考資料
- https://zhuanlan.zhihu.com/p/40987633
- https://www.cnblogs.com/patientcat/p/9720308.html
- https://www.cnblogs.com/penghuwan/p/8166133.html
- http://www.cnblogs.com/penghuwan/p/8057482.html#_label10
數據結構(一)-- 平衡樹