###### tags: `資料結構` `Python` # 資料結構 | AVL Tree 的介紹及實作 | 使用 Python ## 平衡二元樹 為了降低搜尋所花費的時間,二元樹通常是高度越低越好,但是對於經常變動的資料來說,這樣可能會造成二元樹的歪曲 假設我有一陣列 `[1,2,3,4,5]`輸入到二元樹,根據大的放右子樹,小的放左子樹會形成這樣的結構 ![](https://hackmd.io/_uploads/S1aQBU8Eh.png) 可以見到,二元樹極度的歪斜,這又被稱為歪斜樹(Skewed Tree)。最慘這可能把搜尋的效率降低到 $O(n)$ 為了解決這種方式,有了平衡樹的產生,平衡樹會在每次變動資料時整理二元樹的排序,讓他始終保持在最佳化 ## AVL Tree 其中 `AVL Tree` 是最早被發明的自平衡二元搜索樹(self-balancing binary search tree) 他會在每次輸入新的值後改變樹的結構,讓樹保持平衡,讓它保持著每個節點的左右子樹高度差不超過$1$的特性 $H_l$ 表示左子樹高度,$H_r$ 右子樹的高度。兩者高度不能高於$1$ $$|H_l - H_r| > 1 $$ **以下是個不平衡二元樹** 如果你從root節點 來看,右子樹的高度為4,但是左子樹的高度卻是2,`|4-2|=2 > 1` 很明顯的不平衡 ![](https://hackmd.io/_uploads/SyYvlO84h.png) **而這是平衡後的二元樹** 從root來看左子樹高度為3,右子樹高度為2,`|3-2|=1 <= 1` ![](https://hackmd.io/_uploads/rkJ_gu8N2.png) ## 旋轉 AVL樹是一種自平衡二元搜尋樹,左旋和右旋是AVL樹中的兩種基本操作 ### 左旋 左旋是指將某個節點的右子樹作為根節點,並將該節點作為其右子樹的左子樹。這個操作可以用來解決當某個節點的右子樹比左子樹高度更高時,使其保持平衡 把步驟拆分 1. 從 t1 獲取 t2 2. 從 t1 獲取 sub_root 3. 從 t2 獲取 o 4. t2.right 設為 t1 5. t1.parent 設為 t2 6. t1.left 設為 o 7. 如果 o 非 None 則 o.parent 設為 t1 8. t2.parent 設為 sub_root 9. 如果sub_root 是 None 則代表 t2 為 root 10. 如果非 None 則代表 t1 是 sub_root 的左或右子點 11. 更新高度 ```python! def _left_rotate(self, t1): sub_root = t1.parent t2 = t1.right o = t2.left #* 交換 t2.left = t1 t1.parent = t2 t1.right = o if o != None: o.parent = t1 #* 設定 sub_root t2.parent = sub_root if sub_root == None: self.root = t2 else: if sub_root.left == t1: sub_root.left = t2 else: sub_root.right = t2 # 設定變換後的高度 t1.height = self._get_height(self._get_taller_node(t1)) + 1 t2.height = self._get_height(self._get_taller_node(t2)) + 1 ``` ### 右旋 右旋是指將某個節點的左子樹作為根節點,並將該節點作為其左子樹的右子樹。這個操作可以用來解決當某個節點的左子樹比右子樹高度更高時,使其保持平衡 1. 從 t1 獲取 t2 2. 從 t1 獲取 sub_root 3. 從 t2 獲取 o 4. t2.left 設為 t1 5. t1.parent 設為 t2 6. t1.right 設為 o 7. 如果 o 非 None 則 o.parent 設為 t1 8. t2.parent 設為 sub_root 9. 如果sub_root 是 None 則代表 t2 為 root 10. 如果非 None 則代表 t1 是 sub_root 的左或右子點 11. 更新高度 ```python def _right_rotate(self, t1): sub_root = t1.parent t2 = t1.left o = t2.right #* 交換 t2.right = t1 t1.parent = t2 t1.left = o if o != None: o.parent = t1 #* 設定 sub_root t2.parent = sub_root if sub_root == None: self.root = t2 else: if sub_root.left == t1: sub_root.left = t2 else: sub_root.right = t2 # 設定變換後的高度 t1.height = self._get_height(self._get_taller_node(t1)) + 1 t2.height = self._get_height(self._get_taller_node(t2)) + 1 ``` ## 實作 AVL Tree ### 新增 每次新增時可能會打破樹的平衡,所以需要進行檢查和重新排序樹,流程會分成以下三個步驟 1. 輸入新的資料 2. 比較左右兩個子樹的高度 3. 如果大於1,根據歪斜的種類執行相對應的方法 #### 1. 輸入新資料 在輸入新資料後執行檢查 ```python! def _insert(self,value,cur_node): if value < cur_node.value: if cur_node.left == None: cur_node.left = node(value) cur_node.left.parent = cur_node self._inspect_insertion(cur_node.left) # 檢查是否平衡 else: self._insert(value,cur_node.left) elif value > cur_node.value: if cur_node.right == None: cur_node.right = node(value) cur_node.right.parent = cur_node self._inspect_insertion(cur_node.right) # 檢查是否平衡 else: self._insert(value,cur_node.right) else: print("Value 已存在!") ``` #### 2. 比較左右兩個子樹的高度 ```python! def _inspect_insertion(self,cur_node,path=[]): # 當為root節點則返回 if cur_node.parent == None: return # 紀錄每次遞規時的node path = [cur_node] + path # 算出父節點左右子樹的高度 left_height = self.get_height(cur_node.parent.left) right_height = self.get_height(cur_node.parent.right) # 如果左右子樹高度 > 1 則執行 if abs(left_height - right_height) > 1: path=[cur_node.parent] + path self._rebalance_node(path[0], path[1], path[2]) return # 更新當前高度 new_height=1+cur_node.height if new_height>cur_node.parent.height: cur_node.parent.height = new_height # 執行遞規 self._inspect_insertion(cur_node.parent, path) ``` #### 3. 執行方法 執行的方式有分為四個種類,分別為 1. LL,兩次都左 2. RR,兩次都右 3. LR,先左再右 4. RL,先右再左 ##### LL 假設有一不平衡樹,`t2`是`t1`的左子節點,`t3`是`t2`的左子節點,如下圖 ![](https://hackmd.io/_uploads/Bk6ctB84n.jpg) 根據上面的圖來看,它需要進行`右旋` ```python! if t1.left == t2 and t2.left == t3: self._right_rotation(t1) ``` ##### RR RR 也和 LL 一樣,不過需要左右顛倒 假設有一不平衡樹,`t2`是`t1`的右子節點,`t3`是`t2`的右子節點,如下圖 ![](https://hackmd.io/_uploads/ByUitS8Vh.jpg) 根據上面的圖來看,它需要進行`左旋` ```python! if t1.right == t2 and t2.right == t3: self._left_rotation(t1) ``` ##### LR 假設有一不平衡樹,`t2`是`t1`的左子節點,`t3`是`t2`的右子節點,如下圖 ![](https://hackmd.io/_uploads/SywlpPUNn.jpg) 遇到這種你可以先使用`左旋`,再使用`右旋`,不過要注意起始點的位置 ```python! if t1.left == t2 and t2.right == t3: self._left_rotation(t2) self._right_rotation(t1) ``` ##### RL 假設有一不平衡樹,`t2`是`t1`的右子節點,`t3`是`t2`的左子節點,如下圖 ![](https://hackmd.io/_uploads/rJuyTvIEn.jpg) 你可以先使用`右旋`,再使用`左旋` ```python! if t1.right == t2 and t2.left == t3: self._right_rotate(t2) self._left_rotate(t1) ``` ### 刪除 如果要刪除節點可能會打亂樹的平衡,所以在刪除完節點後需要再重新診斷並平衡樹 1. 刪除節點 2. 診斷被刪除節點的父節點左右兩子樹的高度 3. 執行平衡 #### 1. 刪除節點 ```python! def _delete(self, node): if node == None: return #* 計算該節點的子節點數量 def number_of_children(node): number = 0 if node.left != None: number += 1 if node.right != None: number +=1 return number def get_min_value(node): current = node while current.left != None: current = current.left return current parent = node.parent num_child = number_of_children(node) #TODO 沒有child,就刪除節點 if num_child == 0: if parent == None: # 代表 root self.root = None else: if parent.left == node: # 確認節點的位置並從父節點刪除 parent.left = None else: parent.right = None return #TODO 1個child,直接讓子節點篡位 if num_child == 1: child = node.left if node.left != None else node.right if parent == None: # 刪除的節點如果為 root,子節點篡位 self.root = child else: if parent.left == node: # 確認被刪除節點的位置,子節點直接篡位 parent.left = child child.parent = parent else: parent.right = child child.parent = parent #TODO 2個child,獲取要刪除節點右節點的最小值或是左節點的最大值,並代替刪除的節點 if num_child == 2: successor = get_min_value(node.right) node.value = successor.value # 篡位該節點 self._delete(successor) # 刪除該節點原本的位置 # 節點刪除後,處理樹的平衡 self._inspect_deletion(node.parent) ``` #### 2. 診斷被刪除節點的父節點左右兩子樹的高度 :::warning node節點為被刪除節點的父節點 ::: 1. 獲取左右子樹的高度 2. 比較兩者差距是否 > 1 3. 判斷左右兩子節點的高度,高度較高的進行變動 4. 執行平衡方法 ```python! def _inspect_deletion(self, node): #* 比較左右高度 > 1,則執行 #* 從node開始選擇較高的節點 t2,t2中較高的 t3 if node == None: return #* 1. 獲取左右子樹的高度 left = self._get_height(node.left) right = self._get_height(node.right) # 2. 比較兩者差距是否 > 1 if abs(left - right) > 1: # 3. 判斷左右兩子節點的高度,高度較高的進行變動 t2 = self._get_taller_node(node) t3 = self._get_taller_node(t2) # 4. 執行平衡方法,詳情見上 self._rebalance_node(node, t2, t3) #* 往上診斷 self._inspect_deletion(node.parent) def _get_taller_node(self, node): #* 比較左右子節點高度 #* return高度較高的節點 left = self._get_height(node.left) right = self._get_height(node.right) return node.left if left > right else node.right ``` #### 3. 執行平衡 執行平衡方法同插入新節點的方式,詳情如上 ## 完整程式碼 ```python! class Node: def __init__(self, value=None) -> None: self.value = value self.left = None self.right = None self.parent = None # 節點的父節點 self.height = 1 # 節點在樹中的高度 class Binary_search_tree: def __init__(self) -> None: self.root = None def insert(self,value): if self.root == None: self.root = Node(value) else: self._insert(value, self.root) def _insert(self, value, cur_node): #* 如果 insertValue < 當前節點,放左邊 if cur_node.value > value: if cur_node.left == None: cur_node.left = Node(value) cur_node.left.parent = cur_node self._inspect_insertion(cur_node.left) else: self._insert(value, cur_node.left) #* 如果 insertValue > 當前節點,放右邊 elif cur_node.value < value: if cur_node.right == None: cur_node.right = Node(value) cur_node.right.parent = cur_node self._inspect_insertion(cur_node.right) else: self._insert(value, cur_node.right) else: print('已存在節點') def delete(self, value): return self._delete(self.find(value)) def _delete(self, node): if node == None: return #* 計算該節點的子節點數量 def number_of_children(node): number = 0 if node.left != None: number += 1 if node.right != None: number +=1 return number def get_min_value(node): current = node while current.left != None: current = current.left return current parent = node.parent num_child = number_of_children(node) #TODO 沒有child,就刪除節點 if num_child == 0: if parent == None: # 代表 root self.root = None else: if parent.left == node: # 確認節點的位置並從父節點刪除 parent.left = None else: parent.right = None return #TODO 1個child,直接讓子節點篡位 if num_child == 1: child = node.left if node.left != None else node.right if parent == None: # 刪除的節點如果為 root,子節點篡位 self.root = child else: if parent.left == node: # 確認被刪除節點的位置,子節點直接篡位 parent.left = child child.parent = parent else: parent.right = child child.parent = parent #TODO 2個child,獲取要刪除節點右節點的最小值或是左節點的最大值,並代替刪除的節點 if num_child == 2: successor = get_min_value(node.right) node.value = successor.value # 篡位該節點 self._delete(successor) # 刪除該節點原本的位置 # 節點刪除後,處理樹的平衡 self._inspect_deletion(node.parent) def find(self, value): if self.root == None: print('不存在該節點') return None else: return self._find(value, self.root) def _find(self, value, cur_node): #* 思路: 返回相同值,沒有則遞迴查找 if cur_node.value == value: return cur_node elif cur_node.value < value and cur_node.right != None: return self._find(value, cur_node.right) elif cur_node.value > value and cur_node.left != None: return self._find(value, cur_node.left) else: print(f'不存在該數值 {value}') return None def _inspect_insertion(self, cur_node, path=[]): #* 先計算出每個節點距離ROOT的高度 #* 根據節點左右兩點的差值來判斷是否要啟動變動 if cur_node.parent == None: return path = [cur_node] + path left_height = self._get_height(cur_node.parent.left) right_height = self._get_height(cur_node.parent.right) # 如果當前節點的父節點左右子點高度相差 > 1 則啟動變換 if abs(left_height - right_height) > 1: # 準備要調整的三個節點 path = [cur_node.parent] + path self._rebalance_node(path[0], path[1], path[2]) return new_height = cur_node.height + 1 # 父節點的高度一定比子高,如果有歪斜,則採最大的那個 if new_height > cur_node.parent.height: cur_node.parent.height = new_height # 往上探查 self._inspect_insertion(cur_node.parent, path) def _inspect_deletion(self, node): #* 獲取左右子樹的高度 #* 比較左右高度 > 1,則執行 #* 從node開始選擇較高的節點 t2,t2中較高的 t3 if node == None: return left = self._get_height(node.left) right = self._get_height(node.right) if abs(left - right) > 1: t2 = self._get_taller_node(node) t3 = self._get_taller_node(t2) self._rebalance_node(node, t2, t3) #* 往上診斷 self._inspect_deletion(node.parent) def _rebalance_node(self, t1, t2, t3): # T1最上面 T3最下面 #TODO LL | 右 if t1.left == t2 and t2.left == t3: self._right_rotate(t1) #TODO RR | 左 if t1.right == t2 and t2.right == t3: self._left_rotate(t1) #TODO LR | 先右再左 if t1.left == t2 and t2.right == t3: self._left_rotate(t2) self._right_rotate(t1) #TODO RL | 先左再右 if t1.right == t2 and t2.left == t3: self._right_rotate(t2) self._left_rotate(t1) def _right_rotate(self, t1): sub_root = t1.parent t2 = t1.left o = t2.right #* 交換 t2.right = t1 t1.parent = t2 t1.left = o if o != None: o.parent = t1 #* 設定 sub_root t2.parent = sub_root if sub_root == None: # 如果三者的上面是None那一定是Root self.root = t2 else: if sub_root.left == t1: sub_root.left = t2 else: sub_root.right = t2 #* 設定變換後的高度 t1.height = self._get_height(self._get_taller_node(t1)) + 1 t2.height = self._get_height(self._get_taller_node(t2)) + 1 def _left_rotate(self, t1): sub_root = t1.parent t2 = t1.right o = t2.left #* 交換 t2.left = t1 t1.parent = t2 t1.right = o if o != None: o.parent = t1 #* 設定 sub_root t2.parent = sub_root if sub_root == None: self.root = t2 else: if sub_root.left == t1: sub_root.left = t2 else: sub_root.right = t2 #* 設定變換後的高度 t1.height = self._get_height(self._get_taller_node(t1)) + 1 t2.height = self._get_height(self._get_taller_node(t2)) + 1 def _get_height(self, cur_node): return 0 if cur_node == None else cur_node.height def _get_taller_node(self, node): #* 判斷節點左右高度,並return高度較高的節點 left = self._get_height(node.left) right = self._get_height(node.right) return node.left if left > right else node.right ```