Skip to content

Instantly share code, notes, and snippets.

@CandyMi
Created June 10, 2026 11:10
Show Gist options
  • Select an option

  • Save CandyMi/065623bd5ada98ae0d6768e8f9d4354d to your computer and use it in GitHub Desktop.

Select an option

Save CandyMi/065623bd5ada98ae0d6768e8f9d4354d to your computer and use it in GitHub Desktop.
Skip List vs Treap — 排行榜场景评估

Skip List vs Treap — 排行榜场景评估

面向 Leaderboard(排行榜/排名系统),对比两种支持有序遍历的数据结构。

操作复杂度

操作 Treap Skip List (基本) Skip List (带 rank 表)
Insert O(log n) O(log n) O(log n)
Find O(log n) O(log n) O(log n)
Erase O(log n) O(log n) O(log n)
kth (Top N) O(log n) O(n) O(log n)
rank O(log n) O(n) O(log n)
iterate (正向) O(1) next O(1) next O(1) next
iterate (逆序) O(1) prev O(n) O(n)

Treap 天然支持 kth/rank(size 字段做二分下降);Skip List 需要额外维护 rank 表才能做到。 逆序是 Treap 的独特优势——双向遍历共用一个 cctreap_prev 函数,Skip List 只能利用 backward 指针倒退,末端仍需 O(n) 定位。

节点空间与分配

Treap (cctreap_node_t):
  child[2]  = 16B
  pc        =  8B    (parent pointer)
  size      =  8B    (子树节点数)
  priority  =  8B    (xorshift64 随机值)
  ─────────────────
  总 32B  (侵入式,key/score 在外)

Skip List (64 位, p=0.5, 期望 2 层):
  data      =  8B    (指向用户结构体指针)
  level     =  4B
  backward  =  8B
  forward[] = 期望 ~16B (实际按随机层数分配)
  ─────────────────
  ~36B 期望 + 每次 insert 动态分配 forward 数组

侵入式 vs 非侵入式

// Treap — 节点嵌入用户结构体,零分配
struct player { int id; int score; cctreap_node_t node; };
cctreap_insert(&t, &p->node, NULL);

// Skip List — 每次 insert 都要 malloc
struct snode { struct player *p; int level; struct snode *backward, **forward; };
s->forward = malloc(s->level * sizeof(snode*));
skiplist_insert(&sl, s);

100 万次 insert,Treap 无堆分配,Skip List 每节点至少 2 次 malloc——延迟差距通常达 2-3×

逐操作对比

Insert

Treap Skip List
分配 零(侵入式) 至少 2 次 malloc
定位 BST 下降 + 冒泡旋转 层叠指针下降 + 插入
修复 旋转 0~O(log n) 次,每次常数 更新 forward 指针
预期 6-10 ms / 100K ~15-25 ms / 100K

Find / 精确查找

Treap 用函数指针比较(除非 CCTREAP_COMPARE 宏内联),每次比较是间接调用。

Skip List 用直接指针比较——find 上 Skip List 略快(预估 8-12ms vs 11-15ms / 100K)。

kth(第 k 小/大)

排行榜核心操作。取 Top N 时:

# 取 Top 100(高频操作)
treap:     for i in range(100): treap.kth(i)         # 100× O(log n)
skiplist:  从头走 100  forward                       # O(100) ← 反而更快

# 取第 50000 名(中位数附近)
treap:     treap.kth(50000)                           # O(log n) ~17 步
skiplist:  从头走 50000                               # O(n) = 50000 步

Top N(N 很小):Skip List 快于 Treap。 任意排名查询(随机 k):Treap 恒定 O(log n),Skip List 退化为 O(n)。

rank(查排名)

玩家分数变化后,需要知道新排名

treap:     treap.rank(&probe)              # O(log n) 下降累加左子树大小
skiplist:  std::distance(begin, find(p))   # O(n) 从头走到目标

Skip List 做 rank 的唯一办法是往前走——对 100 万人的榜单位于中间的用户,平均要走 50 万步。

Treap 在这方面有数量级优势

逆序遍历

"我后面还有多少人?" "谁在垫底?"

treap:     cctreap_rbegin(&t) → cctreap_prev(p)    # O(1) 开始 + O(1) 每步
skiplist:  走到尾 → backward 倒退                     # O(n) 定位尾 + O(1) 每步

更新分数

两者都需要 erase → 改分 → insert,各 2×O(log n)。无本质差异。

综合评分

维度 Treap Skip List 基本 Skip List + rank
kth/rank ⭐⭐⭐⭐⭐ ⭐⭐ (O(n)) ⭐⭐⭐⭐ (实现复杂)
逆序遍历 ⭐⭐⭐⭐⭐ ⭐ (O(n)+) ⭐ (O(n)+)
insert 延迟 ⭐⭐⭐⭐ (零分配) ⭐⭐⭐ (需 malloc) ⭐⭐ (更多 malloc)
find 延迟 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ (指针跳跃) ⭐⭐⭐⭐⭐
并发友好 ⭐ (树结构) ⭐⭐⭐⭐ (天然支持) ⭐⭐⭐
代码复杂度 ~440 行 ~150 行 ~300 行
内存效率 ⭐⭐⭐⭐⭐ 32B 侵入式 ⭐⭐⭐ ~36B + malloc ⭐⭐ (额外字段)

排行榜场景结论

Treap 在排行榜场景全面占优,核心原因:

  1. kth/rank O(log n)——这是排行榜的命脉操作(Top N、查排名、分位数),Skip List 基本版 O(n),带 rank 版实现复杂且逆序仍差。
  2. 逆序 O(1)——排行榜上下翻页、查看垫底都是高频操作。
  3. 侵入式零分配——100 万榜单位无需 100 万次 malloc,延迟和内存碎片都更低。

Skip List 仅在以下情况值得考虑

  • 高并发读写(分布式排行榜、游戏实时计分)——链表比树更容易实现无锁化
  • 纯正向 Top N(只取前几名,从不查 rank/逆序)——链表从头走 N 步简单且快
  • 实现简单优先于性能(原型阶段、小型项目)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment