🎯 什麼情境該想到我

當你「既要有序、又要能動態增刪」的時候——雜湊表 給你 但沒有順序,排序過的 slice 有順序但插入要 。
★ 但純 BST 在實務上幾乎不能直接用——它會退化。理解它的價值在於它是 紅黑樹、B 樹、跳表的共同基礎。

⚙️ 怎麼用(機制與推導)

性質

對任一節點:左子樹所有值 < 它 < 右子樹所有值。

        8
      /   \
     3     10
    / \      \
   1   6      14
      / \     /
     4   7   13

★ 中序走訪(左 → 根 → 右)會得到排序好的序列 —— 這是 BST 一切能力的來源。

type Node struct {
    key         int
    left, right *Node
}
 
func (n *Node) Search(k int) *Node {
    for n != nil && n.key != k {
        if k < n.key { n = n.left } else { n = n.right }
    }
    return n
}
 
func Insert(n *Node, k int) *Node {
    if n == nil { return &Node{key: k} }
    if k < n.key {
        n.left = Insert(n.left, k)
    } else if k > n.key {
        n.right = Insert(n.right, k)
    }
    return n                     // 相等:視需求決定忽略或計數
}

★ 刪除:三種情況(必考)

① 葉節點           → 直接移除
② 只有一個子節點   → 用該子節點取代自己
③ ★ 有兩個子節點  → 用「中序後繼」(右子樹的最小值)取代自己的值,
                      再去右子樹刪掉那個後繼節點

為什麼用中序後繼:它是「所有比我大的值裡最小的那個」,
放到我的位置後,BST 性質對左右子樹都仍然成立。
(用「中序前驅」——左子樹的最大值——也一樣對。)

★ 而且中序後繼必定沒有左子節點(否則那個左子會更小),所以刪除它必定落入情況 ① 或 ②——遞迴不會無限下去。

func Delete(n *Node, k int) *Node {
    if n == nil { return nil }
    switch {
    case k < n.key:
        n.left = Delete(n.left, k)
    case k > n.key:
        n.right = Delete(n.right, k)
    default:
        if n.left == nil  { return n.right }   // ① ②
        if n.right == nil { return n.left }
        succ := n.right                        // ③ 找中序後繼
        for succ.left != nil { succ = succ.left }
        n.key = succ.key
        n.right = Delete(n.right, succ.key)
    }
    return n
}

支援的操作(這是它勝過雜湊表的地方)

操作複雜度(平衡時)雜湊表做得到嗎
查找/插入/刪除✅
最小/最大(一路往左/右)❌
前驅/後繼❌
★ 範圍查詢 [a, b]❌
★ 有序走訪❌
第 k 小(加 size 欄位)❌

★ 致命問題:退化

所有操作都是 ,而 不保證是 。

依序插入 1, 2, 3, 4, 5:

    1
     \
      2
       \
        3        ← ★ 退化成鏈結串列,h = n,所有操作變成 O(n)
         \
          4
           \
            5

而「已排序的輸入」在真實系統裡極其常見(資料庫匯出、遞增 ID、時間序列)——
所以純 BST 不是「偶爾會慢」,是「在最常見的輸入上一定會慢」。

情況高度
完美平衡
隨機插入(期望)
★ 已排序插入

★ 這就是 紅黑樹、AVL 樹、Treap、跳表存在的全部理由:用旋轉或隨機化強制維持 的高度。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • ★ 不要在生產環境用純 BST。要嘛用自平衡版本,要嘛用別的結構。
  • ★ Go 標準庫沒有有序 map/set。實務選項:
    需求做法
    資料不常變排序過的 slice + 二分搜尋(slices.BinarySearch)——最簡單且快取友善,多數情況的正解
    頻繁增刪且要有序第三方 B-tree(如 google/btree)或跳表
    只要最小/最大二元堆積(container/heap)
  • 快取不友善。每個節點獨立配置,走訪時跳來跳去(同 鏈結串列 的問題)。
    這是 B 樹在資料庫與檔案系統勝出的原因——一個節點裝很多鍵,一次 I/O 讀一整塊。
  • 遞迴實作在退化時會爆堆疊。深度不可控時用迴圈版。
  • 相等鍵要先想清楚:忽略?計數?掛成串列?不決定會產生微妙的 bug。
  • 只要查找不要順序 → 用 雜湊表, 勝過 。

🔗 相關工具