cover

不想寫 AVL Tree 的旋轉?丟硬幣就能平衡的 Skip List 了解一下。Redis 就是這麼選的。

先講結論

Skip List 用多層鏈表加上隨機化達成期望 O(log n) 的查找、插入、刪除。實作比平衡樹簡單得多、天然支援區間查詢、並發場景容易加鎖。Redis 的 Sorted Set(zset)底層就是 Skip List,不是紅黑樹。

結構:多層快速通道

Level 3: HEAD ─────────────────────── 25 ─── null
Level 2: HEAD ────── 7 ──────────── 25 ─── null
Level 1: HEAD ── 3 ── 7 ──── 12 ── 25 ─── null
Level 0: HEAD ── 3 ── 6 ── 7 ── 9 ── 12 ── 19 ── 25 ── null

Level 0 是完整的有序鏈表。每往上一層就跳過更多節點,像是書的目錄:Level 0 是每一頁、Level 1 是章節、Level 2 是大章、Level 3 是總覽。

查找時從最高層開始,遇到比目標大的就往下降一層,逐步縮小搜尋範圍。

查找 19:
Level 3: HEAD → 25(太大,下降)
Level 2: HEAD → 7 → 25(太大,下降)
Level 1: 7 → 12 → 25(太大,下降)
Level 0: 12 → 19 ✓

隨機化:不用旋轉也能平衡

插入新元素時,隨機決定它要出現在幾層。每次丟硬幣,正面就多一層。

int randomLevel() {
    int level = 0;
    while (level < MAX_LEVEL && random.nextDouble() < 0.5)
        level++;
    return level;
}

期望下來,大約一半的節點只在 Level 0,四分之一在 Level 1,八分之一在 Level 2…自然形成「越高層越稀疏」的結構。不需要任何旋轉操作。

是的,這是一個靠隨機達成平衡的資料結構。聽起來不靠譜?數學上期望時間複雜度是嚴格的 O(log n)。最差情況 O(n) 理論上存在,但概率低到不用在意。

為什麼 Redis 選 Skip List 而不是紅黑樹?

Redis 作者 antirez 說過原因:

  1. 實作簡單:Skip List 的 code 比紅黑樹少很多,bug 也少
  2. 區間查詢ZRANGEBYSCORE 需要取一段範圍,Skip List 的有序鏈表天然支援,紅黑樹要做中序遍歷
  3. 並發友善:Skip List 只需要鎖住局部的幾個節點,紅黑樹旋轉可能影響整棵子樹
Skip ListAVL / Red-Black Tree
實作難度
區間查詢天然支援要遍歷
並發加鎖局部鎖困難
平衡方式隨機化旋轉

實際應用

  • Redis Sorted Set(zset):排行榜、延遲佇列
  • LevelDB / RocksDB:MemTable(記憶體中的排序結構)
  • Java ConcurrentSkipListMap:並發安全的有序 Map

Skip List 告訴我們一件事:有時候「差不多隨機」比「精確旋轉」更實用。工程上的最佳解不一定是理論上的最優解。