# [資演] 11- AVL樹 ###### tags: `資演` [AVL樹(Adelson-Velsky and Landis Tree)](https://www.programiz.com/dsa/avl-tree)是一種Binary search tree的實做方式,大部分的實做方式與BST一樣,差異在於AVL樹在生長(增加節點)的過程中會透過計算並調整樹的結構來讓樹維持平衡,而不會導致BST過度傾斜(不平衡)。 要達成這個目的,我們首先需要三個概念: * 節點的高度 * 節點的的balance factor * 高度平衡樹 ## 節點的高度 節點的高度,也就是這個節點到葉節點的最長路徑長度。另一種定義方式是使用遞迴的定義,把葉節點的高度設為 $0$,其他節點的高度定義如下: $$ 高度 = \max(左小孩的高度, 右小孩的高度 )+1 $$事實上,葉節點的高度也適用這個定義,只要把那些空的子節點的高度設為 $-1$ 就行了。 ![](https://hackmd.io/_uploads/Hyd84j0M5.png) ## 節點的 balance factor 對於任一節點 $T$(或者說,以該節點為根的子樹),計算出節點 $T$ 的左右子節點高度,然後把左小孩高度減掉右小孩高度,即為balance factor $\text{BF}(T)$。 balance factor有以下特性: * $\text{BF}(T) < -1$ 右邊subtree比較重,需要將一些右邊節點往左邊調整。 * $-1 \leq \text{BF}(T) \leq 1$ 表示平衡,暫時不用調整。 * $\text{BF}(T) > 1$ 左邊subtree比較重,需要將一些左邊節點往右邊調整。 ![](https://hackmd.io/_uploads/r18n8o0Gq.png) ## 高度平衡樹 一棵樹是高度平衡的(height-balanced),如果它滿足以下條件: * 它的左子樹和右子樹都是高度平衡的 * $|左小孩的高度-右小孩的高度|\leq 1$ 另外,一棵空的樹是高度平衡的。 維持一棵樹的平衡,就是AVL樹最主要的目的。 遵循以下步驟,可以判斷一個二元樹是否平衡:計算根節點左右小孩的高度,得到高度後相減,如果得到的結果 $< -1$ 或 $> 1$,則表示這棵樹有某一側比較重,處於不平衡的狀態,需要調整來讓樹繼續維持平衡。下圖標示為綠色的節點為平衡,紅色為不平衡: ![](https://hackmd.io/_uploads/SJOqppRM5.png) ## AVL樹的旋轉 ![](https://hackmd.io/_uploads/r1G4fHbmc.png) 在遇到不平衡情況時,需要調整樹的節點,稱為旋轉(rotation)。注意旋轉前與旋轉後的樹,需要**保持它們的 in-order traversal 不變**。可以參考[這個動畫](https://en.wikipedia.org/wiki/AVL_tree#/media/File:AVL_Tree_Example.gif)來了解AVL樹是怎樣透過旋轉來調整達到平衡的。 AVL樹的旋轉有兩種基本形式: * 左旋轉(left rotation) * 右旋轉(right rotation) 從這兩種基本形式,衍伸出四種旋轉: * 左左旋轉(left-left rotation, LL) * 右右旋轉(right-right rotation, RR) * 左右旋轉(left-right rotation, LR) * 右左旋轉(right-left rotation, RL) 其中,LL和RR只是進行兩次相同的旋轉,故在此不再做介紹。 ### 左旋轉 考慮一棵原本長這樣的樹: ![](https://hackmd.io/_uploads/rk3bTPZQ9.png) 我們想要在節點 $x$ 進行一次左旋轉,該怎麼做呢?首先,如果節點 $x$ 的右小孩 $y$ 有左子樹 $\beta$,把 $x$ 的右子樹換成 $\beta$: ![](https://hackmd.io/_uploads/H1oRJOWmc.png) 那剩下來的 $y$ 節點和他的右子樹 $\gamma$ 呢?這時候我們要考慮三種狀況: 1. 如果 $x$ 沒有parent(也就是說,$x$ 是根節點) --> 把 $y$ 設成 根節點 2. 如果 $x$ 的 parent 是 $p$,且 $x$ 是 $p$ 的左小孩 --> 把 $y$ 設成 $p$ 的左小孩,取代 $x$ 本來的位置 3. 如果 $x$ 的 parent 是 $p$,且 $x$ 是 $p$ 的右小孩 --> 把 $y$ 設成 $p$ 的右小孩,取代 $x$ 本來的位置 ![](https://hackmd.io/_uploads/rkH6NOWX5.png) 完成以上動作之後,再把以 $x$ 為根的那個子樹設成 $y$ 的左子樹。如此便完成了一次左旋轉。 ![](https://hackmd.io/_uploads/HJlAE_-Q9.png) ### 右旋轉 考慮一棵原本長這樣的樹: ![](https://hackmd.io/_uploads/r1v-8d-mc.png) 要在 $y$ 節點進行一次右旋轉,首先,如果節點 $y$ 的左小孩 $x$ 有右子樹 $\beta$,把 $y$ 的左子樹換成 $\beta$: ![](https://hackmd.io/_uploads/S1iFYcZm9.png) 接著,對於剩下的 $x$ 節點及其左子樹 $\gamma$,我們一樣考慮三種情況: 1. 如果 $y$ 沒有parent(也就是說,$y$ 是根節點) --> 把 $x$ 設成 根節點 2. 如果 $y$ 的 parent 是 $p$,且 $y$ 是 $p$ 的右小孩 --> 把 $x$ 設成 $p$ 的右小孩,取代 $y$ 本來的位置 3. 如果 $y$ 的 parent 是 $p$,且 $y$ 是 $p$ 的左小孩 --> 把 $x$ 設成 $p$ 的左小孩,取代 $y$ 本來的位置。 ![](https://hackmd.io/_uploads/rJdwo5W7q.png) 完成以上動作之後,再把以 $y$ 為根的那個子樹設成 $x$ 的左子樹。如此便完成了一次左旋轉。 ![](https://hackmd.io/_uploads/HkJ_oqZ7q.png) 事實上,右旋轉是跟左旋轉完全對稱的操作,只要記住一種,就能推出另外一種。 ### 左右旋轉 先在 $x$ 進行一次左旋轉: ![](https://hackmd.io/_uploads/B1-Whq-mq.png) 再在 $z$ 進行一次右旋轉: ![](https://hackmd.io/_uploads/SJZNhcW7c.png) 如此便完成了一次左右旋轉。 ### 右左旋轉 先在 $x$ 進行一次右旋轉: ![](https://hackmd.io/_uploads/Syk8n5Zmq.png) 再在 $z$ 進行一次左旋轉: ![](https://hackmd.io/_uploads/SJr_2c-Q5.png) 如此便完成了一次右左旋轉。 ## AVL樹的節點插入操作 AVL樹的插入跟BST的插入如出一轍,但是在插入後需要進行旋轉以達成平衡。完成插入之後,我們必須確認是不是有節點存在違反AVL樹對balance factor的要求。 首先,我們知道,插入的新節點,一定是一個葉節點,並且它的balance factor一定是 $0$。 考慮插入前樹原本長這個樣子: ![](https://hackmd.io/_uploads/H13HPWwmc.png) 現在如果想插入一個節點 $9$,要放在哪裡呢?根據BST的定義,應該把它放在 $8$ 的右小孩。重新計算balance factor,得到: ![](https://hackmd.io/_uploads/SJUWOZvm9.png) 我們發現有些節點不平衡了,需要在一些節點上進行旋轉讓樹維持平衡。這種時候可以分成兩種狀況來看: * 節點的balance factor大於 $1$ --> 左子樹的高度比較高 --> 進行**右旋轉**或**左右旋轉** --> 如果**要插入的元素小於左小孩**,進行右旋轉 --> 否則進行左右旋轉 * 節點的balance factor小於 $-1$ --> 右子樹的高度比較高 --> 進行**左旋轉**或**右左旋轉** --> 如果**要插入的元素大於右小孩**,進行左旋轉 --> 否則進行右左旋轉 在這個案例中,我們發現 $11$ 這個節點需要進行一次左右旋轉: ![](https://hackmd.io/_uploads/HJCIFCPXc.png) 最後得到的樹為: ![](https://hackmd.io/_uploads/r1adFRvmc.png) ## AVL樹的節點刪除操作 AVL樹的刪除操作和BST相同,只是刪除後一樣要透過旋轉來維持平衡。 首先,我們必須先透過尋訪來找到要刪除的那個節點。 對於要刪除的節點有三種情形需要考慮: * 要刪除的節點是葉節點 --> 直接刪除該節點即可 * 要刪除的節點有一個子節點(左小孩或右小孩) --> 把該節點刪除後,由小孩取代該節點原本的位置即可 * 要刪除的節點有兩個子節點(左右小孩都非空) --> 找出該節點的in-order traversal的下一個節點(in-order successer),把它原本的位置抽換成那個節點 進行以上動作之後,再做rebalance(也就是進行適當的旋轉),就完成了AVL樹的節點刪除動作。 例如,我們想在下面的樹刪除掉 $13$ 這個節點: ![](https://hackmd.io/_uploads/Sk0MMWum5.png) 我們發現節點 $13$ 有兩個小孩,而其in-order successer為 $21$,所以我們就用21來取代它原本的位置,並重新計算balance factor: ![](https://hackmd.io/_uploads/rymlRW_Qc.png) 在這邊我們對 $21$ 這個節點進行一次右旋轉: ![](https://hackmd.io/_uploads/rJi2Rbd79.png) 得到: ![](https://hackmd.io/_uploads/Sy-TRb_79.png) ## 程式實作 ``` # AVL tree implementation in Python import sys # Create a tree node class TreeNode(object): def __init__(self, key): self.key = key self.left = None self.right = None self.height = 1 class AVLTree(object): # Function to insert a node def insert_node(self, root, key): # Find the correct location and insert the node if not root: return TreeNode(key) elif key < root.key: root.left = self.insert_node(root.left, key) else: root.right = self.insert_node(root.right, key) root.height = 1 + max(self.getHeight(root.left), self.getHeight(root.right)) # Update the balance factor and balance the tree balanceFactor = self.getBalance(root) if balanceFactor > 1: if key < root.left.key: return self.rightRotate(root) else: root.left = self.leftRotate(root.left) return self.rightRotate(root) if balanceFactor < -1: if key > root.right.key: return self.leftRotate(root) else: root.right = self.rightRotate(root.right) return self.leftRotate(root) return root # Function to delete a node def delete_node(self, root, key): # Find the node to be deleted and remove it if not root: return root elif key < root.key: root.left = self.delete_node(root.left, key) elif key > root.key: root.right = self.delete_node(root.right, key) else: if root.left is None: temp = root.right root = None return temp elif root.right is None: temp = root.left root = None return temp temp = self.getMinValueNode(root.right) root.key = temp.key root.right = self.delete_node(root.right, temp.key) if root is None: return root # Update the balance factor of nodes root.height = 1 + max(self.getHeight(root.left), self.getHeight(root.right)) balanceFactor = self.getBalance(root) # Balance the tree if balanceFactor > 1: if self.getBalance(root.left) >= 0: return self.rightRotate(root) else: root.left = self.leftRotate(root.left) return self.rightRotate(root) if balanceFactor < -1: if self.getBalance(root.right) <= 0: return self.leftRotate(root) else: root.right = self.rightRotate(root.right) return self.leftRotate(root) return root # Function to perform left rotation def leftRotate(self, z): y = z.right T2 = y.left y.left = z z.right = T2 z.height = 1 + max(self.getHeight(z.left), self.getHeight(z.right)) y.height = 1 + max(self.getHeight(y.left), self.getHeight(y.right)) return y # Function to perform right rotation def rightRotate(self, z): y = z.left T3 = y.right y.right = z z.left = T3 z.height = 1 + max(self.getHeight(z.left), self.getHeight(z.right)) y.height = 1 + max(self.getHeight(y.left), self.getHeight(y.right)) return y # Get the height of the node def getHeight(self, root): if not root: return 0 return root.height # Get balance factor of the node def getBalance(self, root): if not root: return 0 return self.getHeight(root.left) - self.getHeight(root.right) def getMinValueNode(self, root): if root is None or root.left is None: return root return self.getMinValueNode(root.left) def preOrder(self, root): if not root: return print("{0} ".format(root.key), end="") self.preOrder(root.left) self.preOrder(root.right) # Print the tree def printHelper(self, currPtr, indent, last): if currPtr != None: sys.stdout.write(indent) if last: sys.stdout.write("R----") indent += " " else: sys.stdout.write("L----") indent += "| " print(currPtr.key) self.printHelper(currPtr.left, indent, False) self.printHelper(currPtr.right, indent, True) ```