← 回首頁

樹堆與伸展樹(Treap / Splay Tree)

要讓搜尋樹不長歪,AVL 和紅黑樹的辦法是嚴格記錄高度或顏色——有點麻煩。這頁的兩種樹改用更「取巧」的辦法: Treap 給每個節點擲一個隨機號碼,強迫樹依隨機順序長,靠運氣讓它大機率保持平衡,程式碼卻簡單很多; Splay 樹乾脆不管平不平衡,而是每次你查過某個節點,就順手把它一路轉到樹頂——於是最近常用的東西 永遠在最上面、下次找超快,像瀏覽器把常看的分頁放在手邊。兩者都放棄了「保證」平衡,換來更簡單、或更貼近 使用習慣的設計。

兩者都靠旋轉維持效率,但換取平衡的手段完全不同:Treap 給每個節點一個隨機優先級,插入時額外維持 「優先級 max-heap」性質,靠隨機性換取期望 O(log n)(不需要像 AVL/紅黑樹那樣追蹤高度或顏色); Splay Tree 則不維護任何額外平衡資訊,而是每次 insert/search 後把剛存取的節點旋轉伸展到根, 靠「常存取節點靠近根部」的局部性換取均攤 O(log n),特別適合存取頻率不均勻的工作負載。

1. 樹堆(Treap,隨機優先級 + 旋轉上浮)

insert(value) 先照 BST 規則逐層比較走到 node == null,建立新節點時額外指派一個 priority (本頁用固定種子 LCG 高位元取樣,決定性重播用;實務 Treap 應使用真隨機亂數,priority 隨機才能保證期望 O(log n),固定種子只是為了讓每次示範都重現相同的旋轉步驟);接著從新節點回溯往上, 只要子節點 priority 大於自身 priority 就違反了「priority 的 max-heap 性質」,旋轉一次把子節點換到 父節點的位置,直到 heap 性質恢復或已升到 root。同時維持「BST 鍵序」與「priority 堆積序」兩個不變量。

固定示範腳本(priority 由固定種子 LCG 高位元取樣,決定性重播;實務為隨機): insert(50) insert(30) insert(70) insert(20)[右旋@30] insert(60) insert(40)[左旋@30],共 2 次旋轉。

操作腳本

    旋轉次數:0 節點數:0
    節點上方文字為 p=優先級(priority,由固定種子 LCG 高位元取樣產生,可重播;實務應為真隨機); 紅色(current)= 當前正在比較/回溯的節點、綠色(visited)= 本次 insert 已走過的插入路徑、 黃色外框(frontier)= 本次旋轉實際涉及的節點。

    新節點 priority

    旋轉紀錄(累積)

    已執行操作紀錄

    2. 伸展樹(Splay Tree,伸展到根)

    insert(key)/search(key) 一樣先做標準 BST 定位(找到節點,或 insert 走到 null 建立新節點; search 未找到則改伸展最後訪問節點);找到目標節點 x 後呼叫 splay(x),依 x 與父親 p、祖父 g 的相對位置反覆旋轉直到 x 成為新 root,分三種情形:Zig(g 不存在,父親就是 root,對 p 單旋一次)、 Zig-Zig(x 與 p 同側,即兩者都是左子或都是右子,先旋轉祖父 g 再旋轉父 p,順序是關鍵)、 Zig-Zag(x 與 p 異側,先旋轉父 p 再旋轉祖父 g,兩次旋轉方向相反)。

    固定示範腳本(實測確認 zig/zig-zig/zig-zag 三型皆至少發生一次): insert(50) insert(30)[zig] insert(70)[zig-zig] insert(20)[zig-zig+zig] insert(60)[zig-zig+zig-zag] search(20)[zig] search(60)[zig],共 12 次旋轉,終態 root=60。

    操作腳本

      旋轉次數:0 節點數:0
      紅色(current)= 當前正在伸展的節點 x;黃色外框(frontier)= 本步伸展涉及的 [父節點, 祖父節點](Zig 時只有父節點); 綠色(finalized)= 本次 insert/search 伸展完成後的新 root。三型伸展:Zig = 父親就是 root,單旋一次; Zig-Zig = x 與父親同側,先轉祖父再轉父親(同方向兩次);Zig-Zag = x 與父親異側,先轉父親再轉祖父(相反方向各一次)。

      搜尋結果

      伸展步驟紀錄(累積)

      已執行操作紀錄