--- title: 'AVL 自平衡樹' disqus: kyleAlien --- AVL 自平衡樹 === ## OverView of Content [TOC] ## AVL 概述 > AVL 是 1962 年由 Adelson-Velsky & Landis 發明,所以命名為 AVL AVL 的特色:**在操作 ++`插入`++、++`移除`++ 時去檢查樹的平衡性,如果高度差超過 1 (任何節點的高度差可以是 1、-1、0) 則進行修正** ### TreeNode 紀錄高度 * AVL 樹中的每個節點都 **必須記錄自身在樹中的高度**;當某個節點被插入二元樹時,才可以即時之察覺不平衡的樹節點 ```java= private static final class AVLTreeNode { public AVLTreeNode left, right; public final int val; // 記錄當前節點的高度 public int height; private AVLTreeNode(int val) { this.val = val; height = 0; } // 左、右子樹的高度差 public int diffHeight() { int leftH = left == null ? -1 : left.height; int rightH = right == null ? -1 : right.height; return leftH - rightH; } // 計算自身結點的高度差 public void computeHeight() { int leftH = left == null ? -1 : left.height; int rightH = right == null ? -1 : right.height; // 1 (自身) + 左、右取最高 height = 1 + Math.max(leftH, rightH); } } ``` ### Insert 插入 - 傾斜 * 在每次 Insert 插入節點時,都需要做 **重新計算自身高的的操作**;並做了一棵 AVL 預設樹,方便 Demo 用 > ![](https://i.imgur.com/KOuRTUz.png) ```java= public class AVLTree { private final AVLTreeNode header; public AVLTree(AVLTreeNode header) { this.header = header; } public AVLTreeNode insert(int val) { _insert(header, val); return header; } private AVLTreeNode _insert(AVLTreeNode root, int val) { if(root == null) { // 返回新節點 return new AVLTreeNode(val); } if(root.val <= val) { root.right = _insert(root.right, val); } else if(root.val > val) { root.left = _insert(root.left, val); } // 重新自計算自身節點 root.computeHeight(); return root; } // Demo 用預設樹 public static AVLTreeNode getDefaultTree() { AVLTreeNode root = new AVLTreeNode(19); AVLTree avlTree = new AVLTree(root); avlTree.insert(14); avlTree.insert(3); avlTree.insert(15); avlTree.insert(53); avlTree.insert(26); avlTree.insert(58); avlTree.insert(29); return root; } } ``` * 接下來使用 AVL 樹進行插入,插入 `27`,並且每個遞迴都重新計算路徑節點個高 (使用 `computeHeight` 函數) > ![](https://i.imgur.com/BYkcw9G.png) 1. **更新自身高度**:當 `27` 插入後,每個路徑 (插入後會經過 4 個節點) 上的 Node 都會更新自身高度 | Node | 未更新前 | 更新高度後 | | - | - | - | | 27 | - | 0 (不用更新,原始就是 0) | | 29 | 0 | 1 | | 26 | 1 | 2 | | 53 | 2 | 3 | | 19 | 3 | 4 | > ![](https://i.imgur.com/5R6YBSJ.png) 2. **高度差**:這邊計算高度差的方式是 `left` - `right`,所以插入 `27` 後每個節點的高度差如下表 | Node | 高度差 | 計算 (`left` - `right`) | | - | - | - | | 29 | 1 | `0 - (-1)` | | 26 | -2 | 右傾斜,`-1 - 1` | | 53 | 2 | 左傾斜,`2 - 0` | | 19 | -2 | 右傾斜,`1 - 3` | > 當前計算方式:Math.abs(高度差) > 1,正數為左傾,負數為右傾 ### Remove 移除 - 傾斜 使用以下預設寫好的二元樹,並準備移除 `58` 這個 Node 節點 > ![](https://i.imgur.com/Pd8hoji.png) * **二元樹移除函數**:以下是一個基礎的二元樹移除函數 (請記得二元樹移除有 3 種可能狀態) ```java= public AVLTreeNode _remove(AVLTreeNode root, int val) { if(root == null) { return null; } if(root.val <= val) { root.right = _remove(root.right, val); } else if(root.val > val) { root.left = _remove(root.left, val); } else { // 1. 已是末端節點 if(root.left == null && root.right == null) { return null; } // 2. 只有單邊 (左 or 右) if(root.right == null) { return root.left; } if(root.left == null) { return root.right; } // 3. 左右子樹都有 AVLTreeNode tmp = root; // 找出右子樹中的最小 root = root.right; while (root != null) { root = root.left; } // 移除最小值 AVLTreeNode fix = _removeMin(tmp.right); // 重新為最小值串上 root.left = tmp.left; root.right = fix; } return root; } private AVLTreeNode _removeMin(AVLTreeNode node) { if(node.left == null) { return node.right; } // 遞迴移除最小值 node.left = _removeMin(node.left); return node; } ``` * 測試後會發現 `58` 這個 Node 節點會 **造成 `53` 左傾斜** | Node | 高度差 | 計算 (`left` - `right`) | | - | - | - | | 29 | 0 | - | | 26 | -2 | 右傾斜,`-1 - 1` | | 53 | 2 | 左傾斜,`2 - 0` | ```java= public static void main(String[] args) { AVLTreeNode root = AVLTreeNode.getDefaultTree(); AVLTree avlTree = new AVLTree(root); avlTree.remove(58); } ``` > ![](https://i.imgur.com/LwMOQU5.png) ## Node rotation 旋轉節點 - 調整傾斜 下圖為一個左傾斜的二元樹,高度為 h;`50` 左子樹高度為 `h-3`,右子樹高度為 `h-1`,其 **高度差就為 +2**,是需要調整的傾斜樹 > e.g `50` 高度為 4,左子樹高度為 1,右子樹高度為 3 > > ![](https://i.imgur.com/q4C9eoo.png) * 上圖左傾斜,可以使用 **右旋** 調整,**以 ++30 為根結點,像右旋轉++** > ![](https://i.imgur.com/AZhFINm.png) ### 二元樹 4 種不平衡 - 輔助函數 * 在只有三個節點的二原樹中,其不平衡有四種可能,透過旋轉最終可以得到相同的平衡二元樹,我們把它分為 2 組來講 1. **僅需一次旋轉** * **左左 `rotate left`**:單右璇,即可變成平衡二元樹 > ![](https://i.imgur.com/jkvwMtA.png) ```java= private AVLTreeNode rotateRight(AVLTreeNode node) { // 找到 new Root AVLTreeNode newRoot = node.left; AVLTreeNode nRootR = newRoot.right; // 將 newRoot 原本的右邊,設定給 node 的左邊 node.left = nRootR; newRoot.right = node; // 重新計算高度 node.computeHeight(); return newRoot; } ``` > 照著圖做出程式 > > ![](https://i.imgur.com/9aS0kQu.png) * **右右 `rotate right`**:單左璇,即可變成平衡二元樹 > ![](https://i.imgur.com/X7IMYZM.png) ```java= private AVLTreeNode rotateLeft(AVLTreeNode node) { // 找到 new Root AVLTreeNode newRoot = node.right; AVLTreeNode nRootL = newRoot.left; // 將 newRoot 原本的左邊,設定給 node 的右邊 node.right = nRootL; newRoot.left = node; // 重新計算高度 node.computeHeight(); return newRoot; } ``` > 照著圖做出程式 > > ![](https://i.imgur.com/XQNCgO5.png) 2. **需要兩次旋轉** * **右左 `rotate right-left`**:先右傾再左傾;必須先右璇,再左旋 > ![](https://i.imgur.com/Vg69jdF.png) ```java= private AVLTreeNode rotateRightLeft(AVLTreeNode node) { // 先定義出重點節點 AVLTreeNode child = node.right; AVLTreeNode newRoot = child.left; // 新節點在末端 // 新節點的左右都需要調整 AVLTreeNode nRootR = newRoot.right; AVLTreeNode nRootL = newRoot.left; // 串接原本新節點的子樹 child.right = nRootR; node.left = nRootL; // 串上新節點的左、右 newRoot.left = child; newRoot.right = node; // 兩棵子樹 重新計算高度 child.computeHeight(); node.computeHeight(); return newRoot; // newRoot 會在外部進行運算 } ``` > 照著圖做出程式 > > ![](https://i.imgur.com/ApZiSl9.png) * **左右 `rotate left-right`**:先左傾再右傾;必須先左旋,再右璇 > ![](https://i.imgur.com/yM1kldR.png) ```java= private AVLTreeNode rotateLeftRight(AVLTreeNode node) { // 先定義出重點節點 AVLTreeNode child = node.left; AVLTreeNode newRoot = child.right; // 新節點在末端 // 新節點的左右都需要調整 AVLTreeNode nRootR = newRoot.right; AVLTreeNode nRootL = newRoot.left; // 串接原本新節點的子樹 child.right = nRootR; node.left = nRootL; // 串上新節點的左、右 newRoot.left = child; newRoot.right = node; // 兩棵子樹 重新計算高度 child.computeHeight(); node.computeHeight(); return newRoot; // newRoot 會在外部進行運算 } ``` > 照著圖做出程式 > > ![](https://i.imgur.com/Vyywh9N.png) * **輔助函數**:可以依據上面發現的 **基礎四個狀況** 撰寫出相對應的輔助函數 | `node.diffHeight()` 結果 | 含意 | | - | - | | 2 | 左傾斜 | | -2 | 右傾斜 | ```java= private AVLTreeNode resolveLeftLeaning(AVLTreeNode node) { // 判斷傾斜程度 if(node.diffHeight() == 2) { if(node.left.diffHeight() >= 0) { // 往 "左左" 傾斜 node = rotateRight(node); } else { // 往 "左右" 傾斜 node = rotateLeftRight(node); } } return node; } private AVLTreeNode resolveRightLeaning(AVLTreeNode node) { // 判斷傾斜程度 if(node.diffHeight() == -2) { if(node.right.diffHeight() >= 0) { // 往 "右右" 傾斜 node = rotateLeft(node); } else { // 往 "右左" 傾斜 node = rotateRightLeft(node); } } return node; } ``` ### Insert 使用輔助函數 * 使用 輔助函數 的時機是 **插入 (insert) 後**,在每次插入後都做檢查,並進行調整 ```java= private AVLTreeNode _insert(AVLTreeNode root, int val) { if(root == null) { return new AVLTreeNode(val); } if(root.val <= val) { root.right = _insert(root.right, val); // 可能右傾,嘗試向右調整 root = resolveRightLeaning(root); } else if(root.val > val) { root.left = _insert(root.left, val); // 可能左傾,嘗試向左調整 root = resolveLeftLeaning(root); } root.computeHeight(); return root; } ``` 調整後結果 > ![](https://i.imgur.com/wQ2kE2I.png) ### Remove 使用輔助函數 * 使用 輔助函數 的時機是 **移除 (Remove) 後**,在每次移除後都做檢查 (移除點都需要重新計算是否需要調整,**有 4 個地方需要調整**),並進行調整 ```java= public AVLTreeNode _remove(AVLTreeNode root, int val) { if(root == null) { return null; } if(root.val <= val) { root.right = _remove(root.right, val); // 插入右邊,可能右偏 root = resolveRightLeaning(root); } else if(root.val > val) { root.left = _remove(root.left, val); // 插入左邊,可能左偏 root = resolveLeftLeaning(root); } else { if(root.left == null && root.right == null) { return null; } if(root.right == null) { return root.left; } if(root.left == null) { return root.right; } AVLTreeNode tmp = root; root = root.right; while (root != null) { root = root.left; } AVLTreeNode fix = _removeMin(tmp.right); // 移除右子樹中最小 root.left = tmp.left; root.right = fix; // 由於移除右邊的某一個節點後,可能導致左傾斜 // 所以計算右邊 root = resolveRightLeaning(root); } root.computeHeight(); return root; } private AVLTreeNode _removeMin(AVLTreeNode node) { if(node.left == null) { return node.right; } node.left = _removeMin(node.left); // 移除右子樹中,左邊的 Node // 可能導致,右傾斜 node = resolveRightLeaning(node); return node; } ``` 1. 移除後的結果 (尚未調整) > ![](https://i.imgur.com/6saunV8.png) 2. 調整後的結果 > ![](https://i.imgur.com/s0fMntI.png) ## AVL 效能分析 * 每次 AVL 樹插入,**需要旋轉的 Node ++永遠不會超過 1 個++** * 每次 AVL 樹移除,**需要旋轉的 Node 有多個,++永遠不會超過 log(N) 次旋轉++** :::success 透過上面兩點可以知道其二元樹仍是 **保持 O(N logN) 的效能** ::: ## Appendix & FAQ :::info ::: ###### tags: `資料結構`