---
# System prepended metadata

title: 【考試向】資料結構筆記（AVL Tree）
tags: [資料結構, 程式設計, 電腦]

---

# 【考試向】資料結構筆記（AVL Tree）

[TOC]

歡迎你點入本篇文章！我是 LukeTseng，本系列文章主要整理自學資料結構的一些知識，如果你喜歡我的文章，麻煩您不吝嗇的在文章底下按下一顆愛心，或是追蹤我唷～

## 高度平衡二元搜尋樹（Height Balanced BST）

學習資料結構時，從基礎的二元搜尋樹（BST）跨越到平衡樹是一個非常關鍵的里程碑，在撰寫演算法或面對高壓的程式解題環境時，維持資料結構的高效能，往往是避免 Time Limit Exceeded（TLE）的核心。

要理解「高度平衡」，得要先知道原本的二元搜尋樹（Binary Search Tree, BST）有什麼致命缺點。

在理想狀態下，BST 的查詢、新增和刪除時間複雜度都是 $O(\log N)$。

但如果我們依序插入一組已經排序好的資料（例如：`1, 2, 3, 4, 5`），這棵樹的節點就會全部往右邊長，退化成一條類似鏈結串列（Linked List）的結構（左或右斜樹 Skewed Tree），此時它的時間複雜度就會跌至 $O(N)$，是我們不樂見的。

高度平衡二元搜尋樹（Height Balanced BST）就是為了解決這個問題而誕生的概念：強迫樹的左右兩側盡量均勻生長，透過在插入或刪除節點時進行動態調整，確保整棵樹的高度始終維持在 $O(\log N)$，從而保證所有基本操作的高效性。

## AVL Tree

高度平衡二元搜尋樹是一個概念，而 AVL Tree 是這個概念在歷史上的第一種具體實作方法。

它是由兩位蘇聯計算機科學家 Georgy Adelson-Velsky 和 Evgenii Landis 於 1962 年共同發表的，為了紀念這兩位發明者，這棵樹便以他們名字的首字母縮寫 AVL 來命名。

### AVL Tree 的定義

一棵樹要能被稱為 AVL Tree，必須同時滿足以下兩個條件：

1. 必為一棵二元搜尋樹：滿足左子節點 < 父節點 < 右子節點的基本大小關係。
2. 嚴格的高度平衡：樹中任意一個節點的「左子樹高度 $h_L$」與「右子樹高度 $h_r$」相差的絕對值 $|h_L - h_R|$，最大只能是 1。（即 $|h_L - h_R| \le 1$）

只要有任何一個節點的左右子樹高度差超過 1，這棵樹就違反了 AVL Tree 的定義，必須立即修正。

### 平衡因子（Balance Factor, BF）

為了判斷 AVL Tree 是否滿足定義，需要一個可以量化的指標，即平衡因子（Balance Factor, BF）。

計算公式：$$BF = h_L - h_R$$

$h_L$ 就是左子樹高度，$h_R$ 為右子樹高度。

在 AVL Tree 中，每一個節點的 BF 值只能是以下三種數字：

1. 1（左邊比右邊高一層）
2. 0（左右一樣高）
3. -1（右邊比左邊高一層）

失衡狀態：當新增或刪除節點後，如果某個節點的 BF 值變成了 2 或 -2，這就失衡了，代表這棵樹某邊長得太重了，必須透過特定的旋轉（Rotation）操作來讓它恢復平衡。

### AVL Tree 加入操作

AVL Tree 的加入操作實際上跟二元搜尋樹是一模一樣的，但這個動作會改變這條路徑上所有祖先節點的子樹高度，進而可能改變它們的平衡因子（BF）。

如果沿著新加入的節點一路往上檢查，發現了第一個 BF 變成 2 或 -2 的節點（稱為失衡節點），這時就必須啟動旋轉機制。

判斷要用哪一種旋轉，關鍵在於：新節點是加在失衡節點的哪一側？可歸納成四種路徑，也就是著名的 LL、RR、LR、RL 型態。

### LL 型態（Left-Left）與單右旋

- 發生條件：新節點被插入在失衡節點的左子節點的左子樹上。
- 狀態特徵：失衡節點的 BF = 2（左邊太重），且它的左子節點 BF = 1。
- 解決動作：單右旋（Single Right Rotation）
    - 概念：因為整棵樹像左邊傾倒，要把失衡節點的左子節點往上提升成新的根節點，原本的失衡節點則順勢往右下沉，變成新根節點的右子節點。
    - 實作：如果原本左子節點有自己的右子樹，因為它要被提升了，這個右子樹必須繼承給下沉的失衡節點當作左子樹（這樣才符合 BST 大小規則）。

例：

![image](https://hackmd.io/_uploads/B1pCUH5pZx.png)

加入新節點 30：

![image](https://hackmd.io/_uploads/H1RlvS5pZl.png)

50 節點的 BF 值為 2（2 - 0 = 2），因此失衡，且插入節點 30 位在 50 的左子節點之左子樹，因此為 LL 型。

調整作法是將 50 的左子節點 40 往上提升變成根節點，而自己去到右邊，成為 40 的右子節點：

![image](https://hackmd.io/_uploads/ryvGwH5T-l.png)

### RR 型態（Right-Right）與單左旋

- 發生條件：新節點被插入在失衡節點的右子節點的右子樹上。
- 狀態特徵：失衡節點的 BF = -2（右邊太重），且它的右子節點 BF = -1。
- 解決動作：單左旋（Single Left Rotation）
    - 概念：LL 的鏡像情況，樹向右邊傾倒，所以我們把失衡節點的右子節點往上提，原本的失衡節點往左下沉，變成新根節點的左子節點。
    - 實作：同理，原本右子節點若有左子樹，必須繼承給下沉的失衡節點當作右子樹。

例：（已加入了 50 進去）

![image](https://hackmd.io/_uploads/By5NwH5pZe.png)

根節點 30 的 BF 是 -2（0-2 = -2），因此失衡，且插入節點 50 位在 40 的右子節點之右子樹，因此為 RR 型。

調整作法是將 40 的右子節點 50 往上提升變成根節點，而自己去到左邊，成為 50 的左子節點：

![image](https://hackmd.io/_uploads/S1OcPScabg.png)

### LR 型態（Left-Right）與先左旋再右旋

- 發生條件：新節點被插入在失衡節點的左子節點的右子樹上。
- 狀態特徵：失衡節點的 BF = 2，但它的左子節點 BF = -1（呈現一個 < 字型的折線）。
- 解決動作：雙旋轉（Left-Right Double Rotation）
    - 概念：因為這是一個折線，直接對失衡節點旋轉無法解決問題，須分兩步走，先把折線拉直，再進行處理。
    - Step 1（局部左旋）：先對失衡節點的左子節點做一次單左旋，這會把原本 < 字型的結構拉直成 / 字型，也就是成功將狀態轉換成 LL 型態。
    - Step 2（整體右旋）：既然變成了 LL 型態，就比照 LL 的做法，對失衡節點做一次單右旋即可恢復平衡。

例：

![image](https://hackmd.io/_uploads/rksddB5pbl.png)

加入節點 45：

![image](https://hackmd.io/_uploads/Bymcdr9T-x.png)

可發現根節點的 BF 為 2，其左子節點為 -1，因此為 LR 型，首先把 < 形狀改成直線，把 45 給往上提：

![image](https://hackmd.io/_uploads/rylrFSc6Wx.png)

接著再做 LL 型的旋轉：

![image](https://hackmd.io/_uploads/S1dDYB5TZx.png)

### RL 型態（Right-Left）與先右旋再左旋

- 發生條件：新節點被插入在失衡節點的右子節點的左子樹上。
- 狀態特徵：失衡節點的 BF = -2，但它的右子節點 BF = 1（呈現一個 > 字型的折線）。
- 解決動作：雙旋轉（Right-Left Double Rotation）
    - 概念：LR 的鏡像情況，一樣分兩步將折線拉直。
        - Step 1（局部右旋）：先對失衡節點的右子節點做一次單右旋，把 > 字型結構拉直成 \ 字型，轉換成 RR 型態。
        - Step 2（整體左旋）：對失衡節點做一次單左旋，完成平衡。

例：

![image](https://hackmd.io/_uploads/r1Bc5Bqp-e.png)

加入節點 55：

![image](https://hackmd.io/_uploads/B1ij5B56-l.png)

發現根節點 50 的 BF 值為 -2，其右子節點 60 的 BF 值為 1，因此是一個 RL 型，先把 > 字形轉為 \，將 55 往上提：

![image](https://hackmd.io/_uploads/BJzfoH5aWe.png)

變成 RR 型後，直接把 55 提升為根節點：

![image](https://hackmd.io/_uploads/ByIVsrc6Wl.png)

### LL、RR、LR、RL 的進階範例

#### LL

現在有一棵二元搜尋樹：

![image](https://hackmd.io/_uploads/H1vCw9ia-x.png)

加入 20 後就打破 AVL Tree 的定義：

![image](https://hackmd.io/_uploads/H10gu9oTWx.png)

以下是每個節點的 BF 值：

![image](https://hackmd.io/_uploads/BJYw_5sTZg.png)

由於失衡節點 BF = 2，且其左子節點與左子節點的左子節點 BF 皆為 1，因此為 LL 型，只需考慮調整 50、40、30。

對於有右子節點的 40 來說，到時候只要給 50 當作左子節點即可（因為 45 < 50）。

調整完後就會是：

![image](https://hackmd.io/_uploads/ByeQtqsaZe.png)

#### RR

現在有一棵二元搜尋樹：

![image](https://hackmd.io/_uploads/HkwBo9oT-x.png)

加入 80 後就打破了 AVL Tree 的平衡：

![image](https://hackmd.io/_uploads/H1xqi9i6-l.png)

由於失衡節點 50 BF = -2，且其右子節點 60 BF = -1，而右子節點的右子樹 70 也是 BF = -1。

對於有左子節點的 60，只要給 50 當右子節點即可（因為 60 > 50）。

調整完後變這樣：

![image](https://hackmd.io/_uploads/Bk-_n9opZe.png)

#### LR

現在有一棵二元搜尋樹：

![image](https://hackmd.io/_uploads/H1zjnci6-l.png)

加入 43 後就打破了 AVL Tree 的平衡：

![image](https://hackmd.io/_uploads/r1R1T9oa-e.png)

失衡節點 50 BF = 2，左子節點 40 BF = -1，而其左子節點的右子樹 45 BF = -1，形成一個 < 字形，因此為 LR 型。

首先將 45 往上提，形成 / 形，至於左子節點就給 40 當作右子節點，如下：

![image](https://hackmd.io/_uploads/rkX5Tcjabe.png)

轉成 LL 型後就依照其方式做旋轉：

![image](https://hackmd.io/_uploads/B19fAqjabl.png)

#### RL

現在有一棵二元搜尋樹：

![image](https://hackmd.io/_uploads/HkXagojT-x.png)

加入 52 後就打破了 AVL Tree 的平衡：

![image](https://hackmd.io/_uploads/H1FXbijabl.png)

失衡節點 50 BF = -2，右子節點 60 BF = 1，而其右子節點的左子樹 55 BF = 1，形成一個 > 字形，因此為 RL 型。

首先將 55 往上提，形成 \ 形，至於左子節點先留下，如下：

![image](https://hackmd.io/_uploads/S1LrzijTWg.png)

然後根據 RR 做旋轉：

![image](https://hackmd.io/_uploads/HJNOGjo6Zg.png)

### AVL Tree 的刪除

首先暫時忘記平衡這件事，把 AVL Tree 當作普通的二元搜尋樹（BST）來執行刪除，根據被刪除節點的子節點數量，會有三種情況：

1. 情境 A：刪除葉節點（Leaf Node，無子節點）
    - 直接將該節點移除，並將其父節點指向它的指標設為 nullptr（或 NULL）。
2. 情境 B：刪除只有一個子節點的節點
    - 直接將該節點移除，並讓它唯一的子節點接替它的位置，與被刪除節點的父節點連接。
3. 情境 C：刪除有兩個子節點的節點
    - 不能直接刪除，否則其兩個子樹會斷線。
    - 解法：去它的左子樹找最大值，或去右子樹找最小值（通常稱為中序前驅或中序後繼），將那個替換節點的值複製到要刪除的節點上。
    - 接著，轉而去刪除那個替換節點，因為替換節點必定最多只有一個子節點，所以問題就成功降級為 情境 A 或 情境 B。

接著會回溯更新高度與平衡因子（BF），不過這在 C 程式裡面就會幫我們做了，對於手寫實作來說就步就會跳過，實際要做的是判斷失衡與旋轉修復。

在回溯的過程中，一旦發現某個祖先節點的 $|BF| \geq 2$，就代表刪除動作導致了失衡，必須立刻旋轉修復。

判斷旋轉型態的方式與插入時略有不同，插入時看的是新節點加在哪裡，刪除時看的是失衡節點哪邊比較重（高）。

假設失衡的節點為 X，它較高的一側子節點為 Y：

- LL 型態（左邊太重）：
    - 條件：X 的 BF = 2，且 Y 的 BF = 1 或 0。
    - 解決：對 X 執行單右旋。
    - 註：Y 的 BF = 0 是刪除操作特有的情況，插入時不會發生，代表 Y 的左右子樹一樣高，旋轉後樹的整體高度不會改變。
- RR 型態（右邊太重）：
    - 條件：X 的 BF = -2，且 Y 的 BF = -1 或 0。
    - 解決：對 X 執行單左旋。
- LR 型態（左子樹的右邊較重）：
    - 條件：X 的 BF = 2，且 Y 的 BF = -1。
    - 解決：先對 Y 執行單左旋，再對 X 執行單右旋。
- RL 型態（右子樹的左邊較重）：
    - 條件：X 的 BF = -2，且 Y 的 BF = 1。
    - 解決：先對 Y 執行單右旋，再對 X 執行單左旋。

舉例：

![image](https://hackmd.io/_uploads/H1JSSb6pbl.png)

現在刪除節點 80，找替代節點 90（其右子樹最小節點）：

![image](https://hackmd.io/_uploads/SJKYBWT6Wg.png)

可發現失衡節點發生在 90 上，且其左子節點 -1，又形成 < 字形，因此為 LR 型，在這邊就不展示過成了，直接 show 結果：

![image](https://hackmd.io/_uploads/BkApSWapWx.png)

### 高度為 h 的 AVL-Tree

高度為 h 的 AVL-Tree 節點數最多幾個？最少幾個？

1. 節點數最多 $2^h - 1$
2. $N_h$ 為高度為 $h$ 的 AVL-Tree 中的最少節點數：
    - $N_1 = 1$
    - $N_2 = 2$
    - $N_h = N_{h - 1} + N_{h - 2} + 1 \ (h > 2)$