cover

BST 退化成鏈表的問題,AVL Tree 用旋轉解決了——代價是每次插入刪除都要多做一點事。

先講結論

AVL Tree = BST + 自動平衡。嚴格保證左右子樹高度差不超過 1,所以搜尋、插入、刪除都是穩定的 O(log n),不會退化。讀多寫少的場景選 AVL,讀寫均衡選 Red-Black Tree。

為什麼 BST 需要平衡?

依序插入 1, 2, 3, 4, 5 到普通 BST:

1                    插到 AVL 裡:
 \
  2                      2
   \                    / \
    3       vs         1   4
     \                    / \
      4                  3   5
       \
        5

高度 5(鏈表)         高度 3(平衡)

左邊搜尋是 O(n),右邊是 O(log n)。差距在資料量大時非常可怕。

平衡因子

BF(節點) = 左子樹高度 - 右子樹高度

AVL 的鐵律:每個節點的 |BF| 1。一旦違反,就觸發旋轉修復。

四種旋轉

不平衡只有四種型態,每種有對應的修復方式:

LL(左左)→ 右旋

      z                 y
     /                 / \
    y       →         x   z
   /
  x

RR(右右)→ 左旋

  z                     y
   \                   / \
    y       →         z   x
     \
      x

LR(左右)→ 先左旋再右旋

    z            z            x
   /            /            / \
  y    →       x    →       y   z
   \          /
    x        y

RL(右左)→ 先右旋再左旋

  z          z              x
   \          \            / \
    y  →       x   →      z   y
   /            \
  x              y

看起來複雜,但規律很明確:單邊偏就單旋,折彎就雙旋。寫 code 時其實就是判斷 BF 和子節點的 BF,然後 call 對應的旋轉函式。

插入流程

  1. 照正常 BST 規則插入
  2. 沿插入路徑往回更新每個節點的高度
  3. 檢查 BF,不平衡就旋轉
插入順序 10, 20, 30:

Step 1: 10       Step 2: 10        Step 3: RR 不平衡 → 左旋
                           \             10              20
                            20             \            /  \
                                            20   →    10   30
                                             \
                                              30

旋轉本身是 O(1)(只改幾個指標),整個插入過程是 O(log n)。

AVL vs Red-Black Tree

AVLRed-Black
平衡嚴格度嚴格(BF 1)寬鬆
搜尋略快(更平衡)略慢
插入/刪除旋轉次數較多旋轉次數較少
適用場景讀多寫少讀寫均衡
實際使用資料庫索引Java TreeMap/TreeSet、Linux kernel

面試問到「為什麼 Java TreeMap 用 Red-Black Tree 不用 AVL Tree?」——因為 Map 通常插入刪除頻繁,Red-Black Tree 的旋轉開銷比較小。

但如果你的場景是「建好之後主要拿來查」(像資料庫索引),AVL Tree 更平衡所以搜尋更快。


AVL Tree 是完美主義者——每次操作都要確保自己完美平衡。Red-Black Tree 則是務實派,差不多就好。兩種都對,看你能接受多少不完美。