高效率搜尋樹
與多路搜尋樹

彈性學習週自學頁 · 最佳二元搜尋樹 · 紅黑樹 · 伸展樹 · B 樹 · B+ 樹

搜尋成本模型|動態規劃|旋轉與著色|多路索引
向下閱讀與操作動畫
自學導覽 · 學習路線

從 BST 到資料庫索引

這份自學頁接在二元搜尋樹、二元堆積與 AVL 樹之後,補上第 10、11 章的核心概念。閱讀重點不是背樹名,而是看每種結構如何控制「搜尋成本」:有些靠機率安排根節點,有些靠旋轉或著色控制高度,有些把一個節點做大來減少磁碟存取次數。

閱讀方法
每一節都先看「定義與成本模型」,再操作動畫。動畫只呈現一條代表性路徑;真正要帶走的是右側規則卡與下方公式框,這些才是考題與實作會用到的判斷依據。
PART 01 · BST 搜尋成本

同一組鍵值,可以有不同的搜尋成本

二元搜尋樹的中序順序固定,但樹形不固定。若每個鍵值被搜尋的機率相同,平均比較次數只看深度;若機率不同,常被查到的鍵值應靠近根節點。

按「單步」比較兩棵 BST 的搜尋成本。
成功搜尋模型 成本

若鍵值為 $a_1 \lt a_2 \lt \cdots \lt a_n$,且搜尋 $a_i$ 的機率為 $p_i$,成功搜尋的期望成本可以看成「每個鍵值的深度加 1,再乘上它的機率」。根越淺,比較次數越少。

成功搜尋
$\sum_i p_i(\operatorname{depth}(a_i)+1)$
深度從 0 算,根節點比較 1 次。
外部節點與失敗搜尋 失敗

在每個空子指標補上一個外部節點,可把「搜尋失敗」也納入成本。若搜尋值落在 $a_i$ 與 $a_{i+1}$ 之間,就會走到對應的外部節點。

路徑長度關係
$E=I+2n$
$I$ 是內部路徑長,$E$ 是外部路徑長,$n$ 是內部節點數。
完整期望成本 p/q

完整模型會把成功搜尋與失敗搜尋都納入成本。$p_i$ 是成功搜尋 $a_i$ 的機率;$q_i$ 是搜尋值落在兩個鍵值間隔中的機率,其中 $q_0$ 表示 $x \lt a_1$,$q_i$ 表示 $a_i \lt x \lt a_{i+1}$,$q_n$ 表示 $x \gt a_n$。

成功 + 失敗
$\sum_{i=1}^{n}p_i(\operatorname{depth}(a_i)+1)+\sum_{i=0}^{n}q_i(\operatorname{depth}(e_i)+1)$
$e_i$ 是對應失敗搜尋的外部節點。
例題怎麼讀
同樣是鍵值 $5,10,15,20,25$,等機率時右樹比較好;但若 $5,10,25$ 的機率是 $0.3,0.3,0.3$,而 $15,20$ 的機率只有 $0.05,0.05$,左樹反而比較好。這就是最佳二元搜尋樹要處理的問題。
PART 02 · 最佳二元搜尋樹

用動態規劃選出每個區間的根節點

最佳二元搜尋樹假設每個成功搜尋與失敗搜尋的機率已知,目標是在所有可能樹形中找出總期望成本最低者。核心想法是:一旦某個鍵值被選為根,左、右兩邊就變成較小的最佳子問題。

這裡的動態規劃是什麼意思
動態規劃是一種「先解小問題、把答案存起來、再組合成大問題」的方法。最佳二元搜尋樹的小問題是連續區間 $T_{ij}$:只考慮 $a_{i+1},\ldots,a_j$ 時,哪個鍵值當根會讓期望成本最低。先算短區間,存下最佳成本 $c_{ij}$ 與根 $r_{ij}$,長區間就能直接查表組合左右子樹,不必重複計算同一段。
固定例題:a = 10, 15, 20, 25。
$T_{ij}$
只含 $a_{i+1},\ldots,a_j$ 的最佳二元搜尋樹;$T_{ii}$ 是空樹。
$w_{ij}$
區間內所有成功與失敗搜尋機率的總和。當整個子樹往下一層,這些機率都會多付一次比較成本。
$c_{ij}$
$T_{ij}$ 的最小期望成本,且 $c_{ii}=0$。
$r_{ij}$
讓 $c_{ij}$ 最小的根節點索引;最後用 $r$ 表把樹形重建出來。
為什麼會加上 $w_{ij}$
假設 $a_k$ 被選為 $T_{ij}$ 的根。左子樹和右子樹原本各自最佳,但接到根下面後,區間內每一次搜尋都會多走一層,所以除了左右成本 $c_{i,k-1}+c_{kj}$,還要再加整個區間的權重 $w_{ij}$。
PART 03 · AVL 樹

用平衡因子維持高度

AVL 樹是高度平衡的二元搜尋樹。每個節點 $v$ 記錄平衡因子 $BF(v)=h_L-h_R$,合法值只允許 $-1,0,1$。插入後若最近失衡祖先 $A$ 的平衡因子變成 $\pm2$,就用四種旋轉之一修復。

選一個 AVL 情境,觀察旋轉前後中序順序不變。
LL 與 RR:單旋轉

新節點落在外側子樹。LL 對 $A$ 右旋,RR 對 $A$ 左旋。

LR 與 RL:雙旋轉

新節點落在內側子樹。LR 先對左子做左旋再對 $A$ 右旋;RL 對稱。

插入時的修復量

沿插入路徑往上找第一個失衡祖先,做一次單旋或雙旋即可恢復該區域高度。

自學檢查

看旋轉前後的中序序列是否相同;若相同,就沒有破壞搜尋樹規則。

月份例子
APR、OCT、JAN 的月份例子只是把抽象的 $A,B,C,Y$ 換成實際字串鍵值:APR 觸發 LL,OCT 觸發 RR,JAN 觸發 LR。自學時先看本頁的抽象旋轉,再對月份樹做同樣的中序順序檢查。
PART 04 · 紅黑樹

用著色規則放寬 AVL 的平衡

紅黑樹是一種擴充二元搜尋樹。它不要求左右高度差最多 1,而是用紅、黑規則限制最長路徑不會超過最短黑高路徑的兩倍,因此高度仍為 $O(\log n)$。

固定插入序列:50, 10, 80, 90, 70, 60, 65, 62。
叔節點為紅

父節點與叔節點改黑,祖父節點改紅,衝突往上傳;若祖父是根,最後強制改黑。

叔節點為黑

依 LL、LR、RR、RL 型態做 AVL 類似旋轉,再重新著色。

邊著色觀點

也可把顏色看成邊的顏色:到外部節點的邊為黑,沒有連續紅邊,黑邊數一致。

為何比 AVL 常用

紅黑樹平衡較寬鬆,插入刪除旋轉次數通常較少,因此常見於標準函式庫的有序映射與集合。

紅黑樹高度
$n \ge 2^r-1,\quad h\le 2r,\quad h\le 2\log_2(n+1)$
搜尋、插入、刪除都沿高度進行,所以最壞時間為 $O(\log n)$。
PART 05 · 伸展樹(補充)

每次操作後把目標伸展到根節點

伸展樹也是二元搜尋樹,但不儲存平衡因子或顏色。搜尋、插入、刪除先像普通搜尋樹一樣進行,接著把起始節點一路旋轉到根。單次操作最壞可到 $O(n)$,但攤銷為 $O(\log n)$。

目標鍵值 5 會透過單旋、同向雙旋與異向雙旋往上。
單旋

若起始節點只有父節點、沒有祖父節點,就做一次旋轉讓它成為根。

同向雙旋

左左或右右型態,先轉祖父再轉父節點,一次上升兩層。

異向雙旋

左右或右左型態,對起始節點做兩次旋轉,仍保持中序順序。

自調整效果

近期常查詢的資料會被拉近根節點,對有局部性的查詢序列特別有用。

自底向上與自頂向下
自底向上是先找到起始節點再往上旋轉;自頂向下是搜尋途中就把樹分成「小於目標」與「大於目標」兩棵暫存樹,最後以目標節點為根重新接回。常見實作觀察是:自頂向下版本能在搜尋途中同步重組,通常比先找完再往上修更直接。
自頂向下步驟 1

從根往目標走,一邊走一邊把小於目標的節點接到小樹,把大於目標的節點接到大樹。

自頂向下步驟 2

遇到同向兩步時先做一次旋轉,避免路徑太斜;到達目標後把小樹接左邊、大樹接右邊。

PART 06 · 多路搜尋樹

一個節點裡可以放多個鍵值

多路搜尋樹把二元搜尋樹「每個節點一個鍵值、最多兩個子樹」推廣成「每個節點多個鍵值、最多 $m$ 個子樹」。搜尋時先在節點內找出目標落在哪個區間,再往對應子樹走。

查找 28:先比較根節點的 20, 40,再進入中間子樹。
正式定義 定義

一個 $m$ 路搜尋樹節點可寫成 $n,A_0,(K_1,A_1),\ldots,(K_n,A_n)$,其中最多有 $m$ 個子樹、所以鍵值數 $0\le n\le m-1$。鍵值滿足 $K_1 \lt K_2 \lt \cdots \lt K_n$。令 $K_0=-\infty$、$K_{n+1}=\infty$,則子樹 $A_i$ 中所有鍵值都必須大於 $K_i$ 且小於 $K_{i+1}$。

節點格式怎麼讀
例如根節點有鍵值 20, 40,可記成 $2,A_0,(20,A_1),(40,A_2)$:第一個 2 表示節點內有兩個鍵值,$A_0$ 放小於 20 的子樹,$A_1$ 放介於 20 與 40 的子樹,$A_2$ 放大於 40 的子樹。a,b,c,d,e 節點的表格就是在練習這種讀法。
容量上界
高度為 $h$、分支度為 $m$ 的多路搜尋樹,最多有 $m^h-1$ 個鍵值。
這個公式說明為什麼提高分支數可以大幅降低高度。
PART 07 · B 樹

保持每個節點至少半滿,所有葉子同層

$m$ 階 B 樹是有容量限制的多路搜尋樹。除了根與外部節點以外,每個內部節點至少有 $\lceil m/2\rceil$ 個子節點,最多 $m$ 個子節點,所有外部節點在同一層。

切換插入與刪除情境,觀察分裂、借用與合併。
3 階
2-3 樹
每個內部節點可有 2 或 3 個子節點。
4 階
2-3-4 樹
每個內部節點可有 2、3 或 4 個子節點。
2 階
滿二元樹
所有內部節點都有 2 個子節點。
高度估計
$2\lceil m/2\rceil^{h-1}-1 \le N \le m^h-1$
下界來自根至少 2 個子節點,其餘內部節點至少半滿。
插入分裂

先插入葉節點;若節點超過容量,取中間鍵值往父節點提升,左右鍵值分成兩個節點。

刪除借用

刪除後若節點鍵值太少,優先向相鄰兄弟借一個鍵值,父節點鍵值同步調整。

刪除合併

若兄弟無法借,就把父節點的一個分隔鍵拉下來與兄弟合併;不足可能往父節點傳。

根節點下降

若合併一路傳到根,且根只剩一個鍵值被拉下來,原根可被移除,樹高下降 1。

左偏與右偏
2-3-4 樹插入 102 時,分裂或調整可能得到 left bias 與 right bias 兩種合法結果。left bias 可把 102 留在左側局部,right bias 可讓 100 留在父節點;只要每個節點容量合法、所有葉子同層、且中序順序正確,兩種結果都仍是合法 B 樹。
PART 08 · B+ 樹

資料都在葉節點,葉節點串成雙向串列

B+ 樹把內部節點當成索引節點,只存分隔鍵值與指標;真正資料放在資料節點,也就是葉節點。所有葉節點用雙向串列串起來,因此範圍查詢找到第一個葉節點後,可以直接往右掃描。

範圍查詢:先走索引找到第一個葉節點,再沿葉節點串列掃描。
索引節點

只放鍵值與指標,用來決定搜尋往哪個子樹走,不直接放完整資料。

資料節點

所有實際資料都在葉節點;內部節點只負責把搜尋導向正確葉節點。

插入

插入到葉節點;若葉節點滿了就分裂,並把新的分隔鍵更新到上層索引。

刪除

刪除後若葉節點不足,先借用;不能借就合併,並同步更新父層索引鍵。

插入範例 分裂

以插入 14 為例,資料應放在第一個葉節點;插入 86 則會讓最後一個葉節點分裂。若葉節點容量超過上限,就分裂成兩個葉節點,並把新葉節點的第一個鍵值複製到父層索引,讓搜尋能導向新的資料節點。

刪除範例 借用/合併

刪除 71 可從相鄰葉節點借用並更新父層分隔鍵;刪除 80 可能需要合併。刪除 32 或刪除 86 時,若父層索引也不足,修復會繼續往上傳。

為什麼範圍查詢快
找單一鍵值時仍需從根走到葉節點,成本是 $O(\log N)$。但若要找一段連續鍵值,只要找到第一個葉節點,後面可以沿葉節點串列順序掃描,不必對每筆資料重新從根搜尋。
PART 09 · 總覽比較

每種搜尋樹解決的是不同瓶頸

不要把所有平衡樹都記成「某種旋轉」。先問:瓶頸是高度?搜尋機率?近期常用資料?磁碟存取?還是範圍查詢?

結構核心想法典型成本適合場景
普通 BST左小右大平均可好,最壞 $O(n)$教學概念、資料量小、或輸入順序已隨機;不適合需要最壞情況保證的核心索引。
最佳 BST依搜尋機率安排根節點建表 $O(n^3)$查詢分布穩定、資料幾乎不更新;例如固定字典、編譯器關鍵字表。
AVL 樹嚴格高度平衡搜尋/插入/刪除 $O(\log n)$搜尋遠多於更新、需要穩定低高度;例如記憶體中的符號表、需控制查詢延遲的集合。
紅黑樹用著色規則放寬平衡搜尋/插入/刪除 $O(\log n)$插入刪除頻繁且仍要 $O(\log n)$ 保證;例如標準函式庫的 map/set、事件集合。
伸展樹操作後把目標拉到根節點攤銷 $O(\log n)$有快取局部性,最近查過的鍵很可能再查;例如互動式編輯器、快取表,但單次操作最壞仍可能很慢。
B 樹高分支、節點半滿、葉同層$O(\log N)$,高度很小磁碟頁面或區塊存取昂貴;一個節點可對應一頁,降低樹高,適合檔案系統與資料庫索引。
B+ 樹資料在葉節點,葉節點串接搜尋 $O(\log N)$,範圍掃描快大量範圍查詢與順序掃描;葉節點串列讓區間查詢、排序輸出、資料庫索引掃描有效率。
怎麼選:查詢機率已知

如果鍵集合固定,而且每個鍵被搜尋的機率可事先估計,最佳 BST 的目標是讓常查的鍵更靠近根。

怎麼選:記憶體內的一般集合

若需要一般用途的有序集合,紅黑樹常是實務預設;若搜尋比例很高、希望高度更緊,AVL 樹較合適。

怎麼選:近期資料重複使用

如果操作序列有快取局部性,伸展樹會把最近用到的資料拉到根附近,用多次操作的平均效果換取彈性。

怎麼選:資料在外部儲存

若主要成本是讀寫磁碟頁面或 SSD 區塊,不應只追求二元樹高度;B 樹與 B+ 樹用高分支節點減少 I/O 次數。

選型口訣
資料全在記憶體且要一般用途,先想到紅黑樹;搜尋遠多於更新,考慮 AVL 樹;查詢分布固定且資料很少更新,考慮最佳 BST;資料大到需要頁面或區塊存取,先想到 B 樹或 B+ 樹;範圍掃描很多時,B+ 樹通常比只存內部鍵值的 B 樹更合適。
考前自我檢查
若題目問「為何高度是對數」,回答 AVL/紅黑/B 樹的高度限制;若題目問「為何平均成本較小」,回答搜尋機率與最佳 BST;若題目問「為何資料庫索引不用普通 BST」,回答磁碟 I/O 與 B/B+ 樹的大分支節點。

與線性結構的操作成本比較

操作排序陣列鏈結串列AVL 樹
搜尋 $x$$O(\log n)$$O(n)$$O(\log n)$
搜尋第 $k$ 筆$O(1)$$O(k)$$O(\log n)$
刪除 $x$$O(n)$$O(1)$(已知位置)$O(\log n)$
插入 $x$$O(n)$$O(1)$(已知位置)$O(\log n)$
依序輸出$O(n)$$O(n)$$O(n)$