二元搜尋樹 — BST 操作、AVL 與紅黑樹自平衡機制 | 資料結構與演算法

2026/06/23
二元搜尋樹 — BST 操作、AVL 與紅黑樹自平衡機制 | 資料結構與演算法

二元搜尋樹(Binary Search Tree,BST) 是在二元樹的基礎上加了「左子樹所有值 < 根 < 右子樹所有值」這一條有序約束,讓搜尋效率從 O(n) 提升至 O(log n)。理解 BST 的搜尋、插入、刪除三大操作,以及 AVL 樹紅黑樹如何自動維持平衡,是資料結構與演算法的重要里程碑。

前言

想像你在查一本字典——你不會從第一頁翻到最後,而是先翻到中間,根據字母判斷要查的字在左邊還是右邊,然後再對半查找。這個「對折再對折」的直覺,就是 BST 的核心思想。

BST 讓每次比較都能排除一半的可能,在平衡狀態下,搜尋一個 n 個元素的 BST 只需要 O(log n) 步。但如果插入順序不當,BST 可能退化成一條鏈結串列,效能驟降至 O(n)——這也是為什麼我們需要 AVL 樹和紅黑樹。

本文學習要點:

  • BST 性質與中序有序性
  • 搜尋、插入、刪除三大操作(含刪除的三種情況)
  • TypeScript 完整 BST 實作(驗證 BST、第 K 小、LCA)
  • AVL 樹四種旋轉:LL、RR、LR、RL
  • 紅黑樹五條性質與核心策略
  • C++ STL std::setstd::map 使用與 AVL 實作
  • 複雜度分析與 Splay Tree、Treap 延伸
  • 五道 LeetCode 練習題解析

核心概念

BST 性質(BST Property)

BST 的本質是一個遞迴定義的有序約束:

對任意節點 N:
  - N.left  所有節點的值 < N.val
  - N.right 所有節點的值 > N.val
  - 左右子樹也各自是 BST(遞迴定義)
  - 中序遍歷(左→根→右)= 升序排列

以一棵具體的 BST 為例,搜尋值 6 的路徑:

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

搜尋 6:
  訪問 8  → 6 < 8,往左
  訪問 3  → 6 > 3,往右
  訪問 6  → 6 == 6,找到!

搜尋路徑:8 → 3 → 6(只需 3 步,而非暴力搜尋的 9 步)

BST 退化問題

按升序插入 1, 2, 3, 4, 5,BST 會退化成右傾鏈結串列:

理想 BST(balanced):     退化 BST(degenerate):
        3                   1
       / \                   \
      2   4                   2
     /     \                   \
    1       5                   3
                                 \
   樹高 = 2                       4
   搜尋 = O(log n)                  \
                                     5
                               樹高 = n-1
                               搜尋 = O(n)

這就是為什麼需要 AVL 樹和紅黑樹——它們在每次插入、刪除後自動重新平衡,保證樹高始終在 O(log n)。

刪除操作的三種情況

刪除是 BST 中最複雜的操作,需要分三種情況處理:

情況 1:目標是葉節點 → 直接刪除(返回 null)

情況 2:目標只有一個子節點 → 用子節點替代

   刪除 3:              結果:
       5                   5
      / \                 / \
     3   7       →       4   7
      \
       4

情況 3:目標有兩個子節點 → 找右子樹最小值(中序後繼)替代

   刪除 5:              找到後繼 6,複製值後刪除 6:
       5                   6
      / \                 / \
     3   8       →       3   8
        / \                 / \
       6   9               7   9
        \
         7

右子樹最小值(中序後繼)保證大於左子樹所有值、小於右子樹其他所有值,完美維持 BST 性質。


JavaScript / TypeScript 實作

基礎 BST 類別(搜尋、插入、刪除、中序)

class BSTNode {
  val: number;
  left: BSTNode | null = null;
  right: BSTNode | null = null;
  constructor(val: number) { this.val = val; }
}

class BST {
  root: BSTNode | null = null;

  // ─── 插入(遞迴):O(h) ────────────────────────────────────────────────────
  insert(val: number): void {
    this.root = this._insert(this.root, val);
  }

  private _insert(node: BSTNode | null, val: number): BSTNode {
    if (!node) return new BSTNode(val);
    if (val < node.val)      node.left  = this._insert(node.left,  val);
    else if (val > node.val) node.right = this._insert(node.right, val);
    // val == node.val:BST 通常不儲存重複值,忽略
    return node;
  }

  // ─── 搜尋(迭代):O(h) ────────────────────────────────────────────────────
  search(val: number): boolean {
    let curr = this.root;
    while (curr) {
      if (val === curr.val) return true;
      curr = val < curr.val ? curr.left : curr.right;
    }
    return false;
  }

  // ─── 刪除:O(h) ────────────────────────────────────────────────────────────
  // 三種情況:
  //   1. 葉節點       → 直接刪除
  //   2. 單子節點     → 子節點替代
  //   3. 雙子節點     → 右子樹最小值(中序後繼)替代,再刪後繼
  delete(val: number): void {
    this.root = this._delete(this.root, val);
  }

  private _delete(node: BSTNode | null, val: number): BSTNode | null {
    if (!node) return null;

    if (val < node.val) {
      node.left  = this._delete(node.left,  val);
    } else if (val > node.val) {
      node.right = this._delete(node.right, val);
    } else {
      // 找到目標節點
      if (!node.left)  return node.right; // 情況 1 & 2
      if (!node.right) return node.left;  // 情況 2

      // 情況 3:找右子樹最小值(一路向左)
      const successor = this._findMin(node.right);
      node.val   = successor.val;                        // 值替換
      node.right = this._delete(node.right, successor.val); // 刪除後繼
    }
    return node;
  }

  private _findMin(node: BSTNode): BSTNode {
    while (node.left) node = node.left;
    return node;
  }

  // ─── 中序遍歷(得到升序序列)───────────────────────────────────────────────
  inorder(): number[] {
    const result: number[] = [];
    const traverse = (node: BSTNode | null): void => {
      if (!node) return;
      traverse(node.left);
      result.push(node.val);
      traverse(node.right);
    };
    traverse(this.root);
    return result;
  }
}

// 測試
const bst = new BST();
[5, 3, 7, 1, 4, 6, 8].forEach(v => bst.insert(v));
console.log(bst.inorder());  // [1, 3, 4, 5, 6, 7, 8](升序)
bst.delete(5);
console.log(bst.inorder());  // [1, 3, 4, 6, 7, 8]

驗證 BST(LeetCode 98)

這是 BST 題目中最常見的陷阱——不能只比較父子節點,必須傳遞全局上下界:

// 方法一:上下界傳遞(推薦)
// 根節點值域為 (-∞, +∞)
// 往左走:上界縮小為 node.val(所有左子樹節點必須 < node.val)
// 往右走:下界縮小為 node.val(所有右子樹節點必須 > node.val)
function isValidBST(root: TreeNode | null): boolean {
  function validate(
    node: TreeNode | null,
    min: number,
    max: number
  ): boolean {
    if (!node) return true;

    // 必須嚴格大於 min、嚴格小於 max(BST 不含重複值)
    if (node.val <= min || node.val >= max) return false;

    return validate(node.left,  min,      node.val) &&
           validate(node.right, node.val, max);
  }
  return validate(root, -Infinity, Infinity);
}

// 陷阱:不能只看 node.left.val < node.val < node.right.val
// 反例(錯誤通過但不是 BST):
//     5
//    / \
//   1   4
//      / \
//     3   6
// 4.left=3 < 4 看起來合法,但 3 < 5(根),違反右子樹所有值 > 5 的約束

// 方法二:中序遍歷,確認序列嚴格遞增
function isValidBSTInorder(root: TreeNode | null): boolean {
  let prev = -Infinity;

  function inorder(node: TreeNode | null): boolean {
    if (!node) return true;
    if (!inorder(node.left)) return false;  // 左子樹不合法
    if (node.val <= prev)    return false;  // 當前值不嚴格大於前值
    prev = node.val;
    return inorder(node.right);
  }

  return inorder(root);
}
// 時間 O(n),空間 O(h)

BST 第 K 小元素(LeetCode 230)

BST 中序遍歷 = 升序序列,第 k 個中序元素即第 k 小:

// 迭代中序遍歷:找到第 k 個立即停止,不需要遍歷全部
// 時間 O(h + k),空間 O(h)
function kthSmallest(root: TreeNode | null, k: number): number {
  const stack: TreeNode[] = [];
  let curr: TreeNode | null = root;
  let count = 0;

  while (curr !== null || stack.length > 0) {
    // 一路向左(往最小值方向)
    while (curr !== null) {
      stack.push(curr);
      curr = curr.left;
    }

    curr = stack.pop()!;
    count++;
    if (count === k) return curr.val; // 找到第 k 小,立即返回

    curr = curr.right;
  }

  return -1; // 不應到達此處(k 合法時)
}

// 進階:若頻繁調用且有大量插入刪除,可在每個節點額外儲存左子樹節點數
// (leftCount),將查詢降至 O(h) 而不需遍歷

BST 最低公共祖先(LeetCode 235)

BST 的 LCA(Lowest Common Ancestor)可以利用 BST 性質直接定位,不需要像普通二元樹那樣完整 DFS:

// 思路:若 p 和 q 都小於 root,LCA 在左子樹
//        若 p 和 q 都大於 root,LCA 在右子樹
//        否則 root 本身就是 LCA(p 和 q 分屬兩側,或其中一個就是 root)
// 時間 O(h),空間 O(1)(迭代版本)
function lowestCommonAncestor(
  root: TreeNode,
  p: TreeNode,
  q: TreeNode
): TreeNode {
  let curr: TreeNode = root;

  while (true) {
    if (p.val < curr.val && q.val < curr.val) {
      curr = curr.left!;  // p, q 都在左子樹
    } else if (p.val > curr.val && q.val > curr.val) {
      curr = curr.right!; // p, q 都在右子樹
    } else {
      return curr;        // 分屬兩側,curr 即為 LCA
    }
  }
}

刪除 BST 節點(LeetCode 450)

// 三種情況分類:葉節點 / 單子節點 / 雙子節點
// 時間 O(h),空間 O(h)(遞迴棧)
function deleteNode(root: TreeNode | null, key: number): TreeNode | null {
  if (!root) return null;

  if (key < root.val) {
    // key 在左子樹
    root.left = deleteNode(root.left, key);
  } else if (key > root.val) {
    // key 在右子樹
    root.right = deleteNode(root.right, key);
  } else {
    // 找到目標節點
    if (!root.left)  return root.right; // 情況 1 & 2
    if (!root.right) return root.left;  // 情況 2

    // 情況 3:找右子樹最小值(一路向左)
    let successor = root.right;
    while (successor.left) successor = successor.left;

    // 將後繼值複製過來,再從右子樹刪除後繼
    root.val   = successor.val;
    root.right = deleteNode(root.right, successor.val);
  }

  return root;
}

自平衡 BST

AVL 樹

AVL 樹(Adelson-Velsky and Landis Tree)要求每個節點的平衡因子(Balance Factor)= 左高 - 右高 ∈ {-1, 0, 1},違反時透過旋轉修正。

四種失衡類型與對應旋轉:

─── LL 型(左左失衡):右旋(Right Rotation)───────────────────────────
失衡:                    修正後:
    z [BF=+2]                y
   /                        / \
  y [BF=+1]               x   z
 /
x
操作:z.left = y.right; y.right = z; 更新高度

─── RR 型(右右失衡):左旋(Left Rotation)───────────────────────────
失衡:                    修正後:
z [BF=-2]                    y
  \                          / \
   y [BF=-1]               z   x
     \
      x
操作:z.right = y.left; y.left = z; 更新高度

─── LR 型(左右失衡):先左旋再右旋──────────────────────────────────────
失衡:                中間步:          修正後:
    z [BF=+2]              z                x
   /                      /               / \
  y [BF=-1]             x               y   z
   \                   /
    x                 y
操作:先對 y 做左旋(RR),再對 z 做右旋(LL)

─── RL 型(右左失衡):先右旋再左旋──────────────────────────────────────
失衡:                中間步:          修正後:
z [BF=-2]                z                x
  \                        \             / \
   y [BF=+1]               x           z   y
  /                          \
 x                             y
操作:先對 y 做右旋(LL),再對 z 做左旋(RR)

AVL 樹 TypeScript 完整實作:

class AVLNode {
  val: number;
  left: AVLNode | null = null;
  right: AVLNode | null = null;
  height: number = 1; // 新節點高度為 1

  constructor(val: number) { this.val = val; }
}

class AVLTree {
  private root: AVLNode | null = null;

  // ─── 輔助:高度與平衡因子 ──────────────────────────────────────────────────
  private height(node: AVLNode | null): number {
    return node?.height ?? 0;
  }

  private updateHeight(node: AVLNode): void {
    node.height = 1 + Math.max(this.height(node.left), this.height(node.right));
  }

  private balanceFactor(node: AVLNode): number {
    return this.height(node.left) - this.height(node.right);
  }

  // ─── 旋轉操作 ──────────────────────────────────────────────────────────────

  // 右旋(Right Rotation):處理 LL 型失衡
  private rotateRight(z: AVLNode): AVLNode {
    const y  = z.left!;
    const T3 = y.right;

    y.right = z;
    z.left  = T3;

    this.updateHeight(z); // z 先更新(z 現在是 y 的子節點)
    this.updateHeight(y);

    return y; // y 成為新的子樹根
  }

  // 左旋(Left Rotation):處理 RR 型失衡
  private rotateLeft(z: AVLNode): AVLNode {
    const y  = z.right!;
    const T2 = y.left;

    y.left  = z;
    z.right = T2;

    this.updateHeight(z);
    this.updateHeight(y);

    return y;
  }

  // ─── 平衡修正(插入/刪除後自底向上調用)─────────────────────────────────────
  private balance(node: AVLNode): AVLNode {
    this.updateHeight(node);
    const bf = this.balanceFactor(node);

    // LL 型
    if (bf > 1  && this.balanceFactor(node.left!)  >= 0)
      return this.rotateRight(node);

    // LR 型:先左旋 node.left,再右旋 node
    if (bf > 1  && this.balanceFactor(node.left!)  <  0) {
      node.left = this.rotateLeft(node.left!);
      return this.rotateRight(node);
    }

    // RR 型
    if (bf < -1 && this.balanceFactor(node.right!) <= 0)
      return this.rotateLeft(node);

    // RL 型:先右旋 node.right,再左旋 node
    if (bf < -1 && this.balanceFactor(node.right!) >  0) {
      node.right = this.rotateRight(node.right!);
      return this.rotateLeft(node);
    }

    return node; // 已平衡,無需旋轉
  }

  // ─── 插入 ──────────────────────────────────────────────────────────────────
  insert(val: number): void {
    this.root = this._insert(this.root, val);
  }

  private _insert(node: AVLNode | null, val: number): AVLNode {
    if (!node) return new AVLNode(val);

    if (val < node.val)      node.left  = this._insert(node.left,  val);
    else if (val > node.val) node.right = this._insert(node.right, val);
    else return node; // 不儲存重複值

    return this.balance(node); // 插入後自底向上修正平衡
  }

  // ─── 刪除 ──────────────────────────────────────────────────────────────────
  delete(val: number): void {
    this.root = this._delete(this.root, val);
  }

  private _delete(node: AVLNode | null, val: number): AVLNode | null {
    if (!node) return null;

    if (val < node.val) {
      node.left  = this._delete(node.left,  val);
    } else if (val > node.val) {
      node.right = this._delete(node.right, val);
    } else {
      if (!node.left)  return node.right;
      if (!node.right) return node.left;

      // 找右子樹最小值替換
      let minRight = node.right;
      while (minRight.left) minRight = minRight.left;
      node.val   = minRight.val;
      node.right = this._delete(node.right, minRight.val);
    }

    return this.balance(node); // 刪除後修正平衡
  }

  inorder(): number[] {
    const result: number[] = [];
    const traverse = (n: AVLNode | null): void => {
      if (!n) return;
      traverse(n.left);
      result.push(n.val);
      traverse(n.right);
    };
    traverse(this.root);
    return result;
  }
}

// 測試:按升序插入(普通 BST 會退化,AVL 不會)
const avl = new AVLTree();
[1, 2, 3, 4, 5].forEach(v => avl.insert(v));
console.log(avl.inorder()); // [1, 2, 3, 4, 5](平衡後仍是 O(log n) 高度)

紅黑樹(Red-Black Tree)

紅黑樹是另一種自平衡 BST,用顏色標記代替精確的高度控制。相較於 AVL 樹的嚴格平衡,紅黑樹的規則更寬鬆,插入刪除效率更高,因此 Java TreeMap、C++ std::mapstd::set 均採用紅黑樹實作。

五條性質(缺一不可):

  1. 每個節點是紅色黑色
  2. 根節點是黑色
  3. 葉節點(NIL 節點)是黑色
  4. 紅色節點的子節點必須是黑色(不能有兩個連續紅節點)
  5. 從任意節點到其所有後代葉節點的路徑上,黑色節點數量相同(黑高度相同)

為什麼這五條能保證 O(log n)?

由性質 4(紅不連續)和性質 5(黑高度相同),可以推導出:最長路徑(紅黑交替)不超過最短路徑(全黑)的 2 倍。設黑高度為 bh,則樹高 h ≤ 2 * bh = O(log n)。

紅黑樹 vs. AVL 樹的選擇:

比較項目AVL 樹紅黑樹
平衡嚴格度嚴格(高度差 ≤ 1)較寬鬆(最長路徑 ≤ 2 倍最短)
搜尋效能稍快(樹更矮)稍慢
插入旋轉次數O(log n) 次旋轉O(1) 次旋轉(均攤)
刪除旋轉次數O(log n) 次旋轉O(1) 次旋轉(均攤)
適合場景讀多寫少讀寫均衡
實際應用Java TreeMap、C++ std::map

C++ 對照

STL 有序容器(底層為紅黑樹)

#include <map>
#include <set>
#include <iostream>

void stdSetExample() {
  std::set<int> s = {5, 3, 8, 1, 4, 7, 9};

  // lower_bound:第一個 >= key 的迭代器(閉)
  auto it = s.lower_bound(4);
  std::cout << *it << "\n";   // 4

  // upper_bound:第一個 > key 的迭代器(開)
  it = s.upper_bound(4);
  std::cout << *it << "\n";   // 5

  // 範圍查詢:[3, 7] 之間的所有元素
  auto lo = s.lower_bound(3);
  auto hi = s.upper_bound(7);
  for (auto i = lo; i != hi; ++i) {
    std::cout << *i << " "; // 3 4 5 7
  }
}

void stdMapExample() {
  std::map<std::string, int> freq;

  // 插入/更新:O(log n)
  freq["apple"]++;
  freq["banana"] += 2;
  freq["apple"] += 3;

  // 有序迭代(按 key 的字典序升序)
  for (const auto& [word, count] : freq) {
    std::cout << word << ": " << count << "\n";
    // apple: 4
    // banana: 2
  }

  // lower_bound 在 map 中使用
  auto it = freq.lower_bound("b"); // 指向 "banana"
  std::cout << it->first << "\n";   // banana
}

裸指標 BST 與 LeetCode 題目

#include <algorithm>
#include <optional>
#include <stack>

// ─── 裸指標 BST(對應 LeetCode 介面)────────────────────────────────────────
struct TreeNode {
  int val;
  TreeNode* left;
  TreeNode* right;
  TreeNode(int v = 0, TreeNode* l = nullptr, TreeNode* r = nullptr)
      : val(v), left(l), right(r) {}
};

// ─── BST 插入、搜尋、刪除 ─────────────────────────────────────────────────────
class BST {
 public:
  TreeNode* insert(TreeNode* root, int val) {
    if (!root) return new TreeNode(val);
    if (val < root->val)       root->left  = insert(root->left,  val);
    else if (val > root->val)  root->right = insert(root->right, val);
    return root;
  }

  bool search(TreeNode* root, int val) {
    while (root) {
      if (val == root->val) return true;
      root = (val < root->val) ? root->left : root->right;
    }
    return false;
  }

  TreeNode* deleteNode(TreeNode* root, int val) {
    if (!root) return nullptr;

    if (val < root->val) {
      root->left  = deleteNode(root->left,  val);
    } else if (val > root->val) {
      root->right = deleteNode(root->right, val);
    } else {
      if (!root->left)  return root->right;
      if (!root->right) return root->left;

      // 找右子樹最小值(中序後繼)
      TreeNode* succ = root->right;
      while (succ->left) succ = succ->left;
      root->val   = succ->val;
      root->right = deleteNode(root->right, succ->val);
    }
    return root;
  }
};

// ─── 驗證 BST(LeetCode 98)───────────────────────────────────────────────────
class Solution98 {
 public:
  bool isValidBST(TreeNode* root) {
    return check(root, std::nullopt, std::nullopt);
  }

 private:
  bool check(TreeNode* node,
             std::optional<long> minVal,
             std::optional<long> maxVal) {
    if (!node) return true;
    if (minVal.has_value() && node->val <= minVal.value()) return false;
    if (maxVal.has_value() && node->val >= maxVal.value()) return false;
    return check(node->left,  minVal, node->val) &&
           check(node->right, node->val, maxVal);
  }
};

// ─── 第 K 小元素(LeetCode 230)──────────────────────────────────────────────
class Solution230 {
 public:
  int kthSmallest(TreeNode* root, int k) {
    std::stack<TreeNode*> stk;
    TreeNode* curr = root;
    int count = 0;

    while (curr || !stk.empty()) {
      while (curr) {
        stk.push(curr);
        curr = curr->left;
      }
      curr = stk.top(); stk.pop();
      if (++count == k) return curr->val;
      curr = curr->right;
    }
    return -1;
  }
};

// ─── AVL Tree C++ 實作 ────────────────────────────────────────────────────────
struct AVLNode {
  int val, height;
  AVLNode* left;
  AVLNode* right;
  AVLNode(int v) : val(v), height(1), left(nullptr), right(nullptr) {}
};

class AVLTree {
  int height(AVLNode* n) { return n ? n->height : 0; }

  void updateHeight(AVLNode* n) {
    n->height = 1 + std::max(height(n->left), height(n->right));
  }

  int bf(AVLNode* n) { return height(n->left) - height(n->right); }

  AVLNode* rotateRight(AVLNode* z) {
    AVLNode* y  = z->left;
    AVLNode* T3 = y->right;
    y->right = z; z->left = T3;
    updateHeight(z); updateHeight(y);
    return y;
  }

  AVLNode* rotateLeft(AVLNode* z) {
    AVLNode* y  = z->right;
    AVLNode* T2 = y->left;
    y->left = z; z->right = T2;
    updateHeight(z); updateHeight(y);
    return y;
  }

  AVLNode* balance(AVLNode* n) {
    updateHeight(n);
    int b = bf(n);

    if (b >  1 && bf(n->left)  >= 0) return rotateRight(n);  // LL
    if (b >  1 && bf(n->left)  <  0) {                        // LR
      n->left = rotateLeft(n->left);
      return rotateRight(n);
    }
    if (b < -1 && bf(n->right) <= 0) return rotateLeft(n);   // RR
    if (b < -1 && bf(n->right) >  0) {                        // RL
      n->right = rotateRight(n->right);
      return rotateLeft(n);
    }
    return n;
  }

  AVLNode* _insert(AVLNode* n, int val) {
    if (!n) return new AVLNode(val);
    if (val < n->val)      n->left  = _insert(n->left,  val);
    else if (val > n->val) n->right = _insert(n->right, val);
    else return n;
    return balance(n);
  }

 public:
  AVLNode* root = nullptr;
  void insert(int val) { root = _insert(root, val); }
};

複雜度分析

操作BST(平均)BST(最壞,退化)AVL Tree紅黑樹
SearchO(log n)O(n)O(log n)O(log n)
InsertO(log n)O(n)O(log n)O(log n)
DeleteO(log n)O(n)O(log n)O(log n)
中序遍歷O(n)O(n)O(n)O(n)
空間O(n)O(n)O(n)O(n)
旋轉次數(插入)O(log n)O(1) 均攤
旋轉次數(刪除)O(log n)O(1) 均攤

h 為樹高。普通 BST 的 h 介於 O(log n)(平衡)與 O(n)(退化)之間;AVL 樹嚴格保證 h = O(log n);紅黑樹保證 h ≤ 2 * log₂(n+1),實際上也是 O(log n)。


變體與延伸

Splay Tree(伸展樹)

Splay Tree 是一種自調整 BST(Self-Adjusting BST)。每次存取(搜尋、插入、刪除)都會把目標節點「伸展(Splaying)」到根節點——通過一系列旋轉。

核心優勢:

  • 均攤時間 O(log n)(單次操作最壞 O(n),但連續 m 次操作總共 O(m log n))
  • 對快取友好(最近存取的節點在根附近,下次存取更快)
  • 實作比 AVL 和紅黑樹簡單

適合場景:快取置換、局部性強的存取模式(少數熱點資料被頻繁存取)。

Treap(樹 + 堆)

Treap 是 BST 與最大堆積的結合:每個節點有一個鍵值(key)和一個隨機優先值(priority)。對 key 滿足 BST 性質,對 priority 滿足 Heap 性質。

為什麼有效? 隨機 priority 使 Treap 的期望形狀接近隨機二元搜尋樹(Random BST),期望樹高為 O(log n),且不需要複雜的旋轉邏輯——只要維持 heap 性質,旋轉自然而然發生。

適合場景:競程題目、需要簡單實作但又想要期望 O(log n) 效能的場景。


面試考點

驗證 BST 的常見陷阱

// 錯誤做法:只看父子節點關係
function isValidBSTWrong(root: TreeNode | null): boolean {
  if (!root) return true;
  // 這樣無法偵測到「右子樹的左後代比根節點還小」的情況
  const leftOk  = !root.left  || root.left.val  < root.val;
  const rightOk = !root.right || root.right.val > root.val;
  return leftOk && rightOk &&
         isValidBSTWrong(root.left) && isValidBSTWrong(root.right);
}
// 對以下輸入錯誤通過:[5, 1, 4, null, null, 3, 6]
// 4 的左子節點 3 < 4 符合局部條件,但 3 < 5(根),違反整棵樹的 BST 性質

// 正確做法:傳遞全局上下界
function isValidBSTCorrect(root: TreeNode | null): boolean {
  function check(node: TreeNode | null, min: number, max: number): boolean {
    if (!node) return true;
    if (node.val <= min || node.val >= max) return false;
    return check(node.left, min, node.val) && check(node.right, node.val, max);
  }
  return check(root, -Infinity, Infinity);
}

整數溢位防護

LeetCode 98 的節點值可以是 INT_MININT_MAX,若用 number 類型(JavaScript 的雙精度浮點數)作為邊界:

// TypeScript 中 -Infinity 和 Infinity 安全處理整數邊界
// 不會有溢位問題(JavaScript number 範圍為 -(2^53-1) 到 2^53-1)

// C++ 版本需要特別注意:
// 錯誤:用 INT_MIN / INT_MAX 作邊界 → 比較時 node->val <= INT_MIN 可能溢位
// 正確:用 std::optional<long> 或 LLONG_MIN / LLONG_MAX

刪除後繼時忘記遞迴刪除

// 常見錯誤:複製後繼值後忘記從右子樹刪除後繼
private _deleteWrong(node: BSTNode, val: number): BSTNode | null {
  // ... 找到後繼 ...
  node.val = successor.val; // 值複製
  // 忘記這一行!樹中出現重複值!
  // node.right = this._delete(node.right, successor.val);
  return node;
}

LeetCode 練習

LeetCode 98 — Validate Binary Search Tree(Medium)

function isValidBST(root: TreeNode | null): boolean {
  function validate(node: TreeNode | null, min: number, max: number): boolean {
    if (!node) return true;
    if (node.val <= min || node.val >= max) return false;
    return validate(node.left,  min,      node.val) &&
           validate(node.right, node.val, max);
  }
  return validate(root, -Infinity, Infinity);
}
// 時間 O(n),空間 O(h)

LeetCode 230 — Kth Smallest Element in a BST(Medium)

function kthSmallest(root: TreeNode | null, k: number): number {
  const stack: TreeNode[] = [];
  let curr: TreeNode | null = root;
  let count = 0;

  while (curr !== null || stack.length > 0) {
    while (curr !== null) { stack.push(curr); curr = curr.left; }
    curr = stack.pop()!;
    if (++count === k) return curr.val;
    curr = curr.right;
  }
  return -1;
}
// 時間 O(h + k),空間 O(h)

LeetCode 235 — Lowest Common Ancestor of BST(Medium)

function lowestCommonAncestor(
  root: TreeNode, p: TreeNode, q: TreeNode
): TreeNode {
  let curr: TreeNode = root;
  while (true) {
    if (p.val < curr.val && q.val < curr.val)      curr = curr.left!;
    else if (p.val > curr.val && q.val > curr.val) curr = curr.right!;
    else return curr;
  }
}
// 時間 O(h),空間 O(1)

LeetCode 108 — Convert Sorted Array to BST(Easy)

// 中間元素作為根,分治建構高度平衡的 BST
function sortedArrayToBST(nums: number[]): TreeNode | null {
  function build(left: number, right: number): TreeNode | null {
    if (left > right) return null;
    const mid = Math.floor((left + right) / 2);
    const node = new TreeNode(nums[mid]);
    node.left  = build(left, mid - 1);
    node.right = build(mid + 1, right);
    return node;
  }
  return build(0, nums.length - 1);
}
// 時間 O(n),空間 O(log n)(遞迴棧深度 = 樹高)

LeetCode 450 — Delete Node in a BST(Medium)

function deleteNode(root: TreeNode | null, key: number): TreeNode | null {
  if (!root) return null;
  if (key < root.val) {
    root.left  = deleteNode(root.left,  key);
  } else if (key > root.val) {
    root.right = deleteNode(root.right, key);
  } else {
    if (!root.left)  return root.right;
    if (!root.right) return root.left;
    let succ = root.right;
    while (succ.left) succ = succ.left;
    root.val   = succ.val;
    root.right = deleteNode(root.right, succ.val);
  }
  return root;
}
// 時間 O(h),空間 O(h)

總結

二元搜尋樹是「有序」與「樹」的完美結合,讓字典查詢、範圍查詢、有序統計等問題都能在 O(log n) 內解決。回顧本文的核心知識點:

  1. BST 三大操作:搜尋(利用有序性逐層縮小範圍)、插入(找到正確位置插入)、刪除(三種情況,雙子節點用中序後繼替換)。

  2. 中序有序性:BST 的中序遍歷恆為升序,這讓第 K 小、驗證 BST 等問題有了優雅的解法。

  3. 退化問題與自平衡:有序插入會讓 BST 退化為 O(n),AVL 樹(嚴格高度差 ≤ 1)和紅黑樹(黑高度相同+紅不連續)各自用不同策略保證 O(log n)。

  4. 選擇依據:讀多寫少選 AVL(搜尋更快);讀寫均衡選紅黑樹(插入刪除旋轉少);工業實際幾乎都用紅黑樹(C++ STL、Java TreeMap)。

  5. 四大面試陷阱:驗證 BST 必須傳全局上下界、刪除後繼後要遞迴刪除、AVL 旋轉後先更新子節點再更新父節點高度、整數邊界溢位問題。

掌握 BST 及其自平衡變體,是攻克更進階資料結構(Segment Tree、B+ Tree 資料庫索引)的重要基石。希望這篇文章能幫助你建立清晰的 BST 思維,在面試中自信地處理各種樹題。如果有任何問題或疑惑,歡迎至 聯絡頁面 留言交流。

下一篇預告:我們將進入 字典樹(Trie) 的世界,探討如何用多叉樹高效解決字串前綴搜尋、自動補全與單詞過濾等問題。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。