DSA DSA In Depth
0%
lesson skip-listsprobabilisticindexing

Skip Lists: Probabilistic Multi-Level Indexing & O(log N) Search Proof

7 min read Module 1.3: Linked Structures, Monotonic Stacks/Queues & Cache-Conscious Lists Updated Oct 23, 2026
📄 Cheat Sheet
MIT 6.046J Lecture 12: Skip Lists — Probabilistic Indexing & High-Probability O(log N) Proof
MIT OpenCourseWare (Erik Demaine) MIT 6.046J Lecture 12: Skip Lists — Probabilistic Indexing & High-Probability O(log N) Proof Watch on YouTube ↗

Erik Demaine builds Skip Lists from linked lists of express lanes and proves with high probability (via backwards coin-flip analysis) that search takes O(log N) steps.

0:00 / --:--

Probabilistic Express Lanes & The Backwards Search Proof

In a sorted singly linked list, even though the keys are ordered, binary search is impossible because we lack random access. Suppose we construct a hierarchy of linked lists $L_0, L_1, \dots, L_{H}$: level $L_0$ contains all $N$ elements, and each element present in level $L_k$ is independently promoted to level $L_{k+1}$ with probability $p \in (0, 1)$ (typically $p = 1/2$ or $p = 1/4$). Search starts at the highest level of the sentinel head node: at the current node $u$ and level $k$, we advance horizontally (u = u->forward[k]) as long as u->forward[k]->key < target; when the next key would overshoot target (or is nullptr), we drop down vertically to level $k - 1$.

How do we prove that the expected search cost is $O(\log N)$? Follow Pugh's elegant Backwards Analysis: trace the search path in reverse, starting from the found node at level $0$ and climbing up and left back to the sentinel head at level $H$. At any node on the reverse path at level $k$:

  • Did we arrive at this node by dropping down from level $k+1$? That happens if and only if this node's tower was promoted to level $k+1$, which occurs with probability $p$ (we climb UP 1 level).
  • Otherwise, with probability $1 - p$, this node's tower stopped at level $k$, so the forward search must have reached it horizontally from the left (we step LEFT 1 node at level $k$).
Probabilistic Invariant: Expected Pointers per Node $= \frac{1}{1 - p}$ & Expected Search Cost $= \frac{\log_{1/p} N}{p}$

Let $C(K)$ be the expected number of reverse steps required to climb $K$ levels. In one step ($+1$), with probability $p$ we climb up to $K - 1$ remaining levels, and with probability $1 - p$ we move left and still have $K$ levels to climb:

$$C(K) = 1 + p \cdot C(K - 1) + (1 - p) \cdot C(K) \implies p \cdot C(K) = 1 + p \cdot C(K - 1) \implies C(K) = \frac{K}{p}$$

Since the expected number of nodes at level $L = \log_{1/p} N$ is $N \cdot p^L = 1$, setting $K = \log_{1/p} N$ yields an expected search path length of $\frac{\log_{1/p} N}{p} = O(\log N)$! Meanwhile, the number of forward pointers in a node's tower follows a Geometric($1 - p$) distribution with expected height:

$$\mathbb{E}[\text{height}] = \sum_{k=0}^{\infty} p^k = \frac{1}{1 - p}$$

For $p = 1/2$, each node stores on average $\frac{1}{1 - 1/2} = 2.0$ pointers. For $p = 1/4$ (used by Redis zskiplist and LevelDB / RocksDB MemTable), each node stores on average only $\frac{1}{1 - 1/4} = 1.333$ pointers—less pointer overhead than a binary tree ($2.0$ pointers/node)!

Skip List Parameter Trade-Offs ($p = 1/2$ vs. $p = 1/4$ vs. Balanced BSTs)

Structure Avg Pointers / Node Expected Comparisons Insert Rebalancing Lock-Free Concurrent Insert
Skip List ($p = 1/2$) $2.00$ pointers $2 \log_2 N$ Local pointer splice (0 rotations) Trivial bottom-up atomic CAS
Skip List ($p = 1/4$, Redis / RocksDB) $1.33$ pointers $4 \log_4 N = 2 \log_2 N$ Local pointer splice (0 rotations) Trivial bottom-up atomic CAS
Red-Black / AVL Tree $2.00 - 3.00$ pointers (+ color/parent) $\approx 1.0 - 1.44 \log_2 N$ Up to $O(\log N)$ tree rotations Extremely hard (multi-word rotation locks)
B+Tree (Fanout $B = 16$) $\approx 1.1$ per key $\log_2 N$ (in-cache) Node split/merge Latch crabbing / Bw-Tree

Visualizing Express Lanes & The update[0..level] Predecessor Splice Vector

Skip List Search & Insert for Key 42 (Highlighted Orange = update[k] Predecessors Where Search Drops Down) HEAD L2 HEAD L1 HEAD L0 17 L1 17 L0 31 L0 NEW 42 L1 NEW 42 L0 55 L2 55 L1 55 L0 update[] captures rightmost node < 42 at each level: update[2]=HEAD, update[1]=17, update[0]=31 Rank / Order Statistics: Storing span[k] (number of L0 nodes skipped by forward[k]) gives O(log N) k-th element lookup!

Why RocksDB & Java ConcurrentSkipListMap Use Skip Lists Over Red-Black Trees

flowchart LR A["Concurrent Writers in
RocksDB MemTable"] --> B["Red-Black Tree:
Insert triggers bottom-up rotations
Mutates root/grandparent pointers
Requires coarse subtree locks"] A --> C["Lock-Free Skip List:
Node height chosen via PRNG
Link Level 0 via atomic CAS first!
Then link higher levels 1..h via CAS"]
Hardware Reality: Flexible Array Members (forward[]) & Single-Allocation Towers

A naive Skip List implementation defines struct Node { int key; std::vector<Node*> forward; }. That design is a cache disaster: every node incurs two separate heap allocations (one for the Node and one for the std::vector's dynamic buffer), and stepping u->forward[k] requires an extra pointer dereference to reach the vector's heap array! Production Skip Lists (such as Redis's zskiplistNode and LevelDB's SkipList::Node) allocate the Node header and its exact $h$-element Node* forward[h] array in a single contiguous block of memory of size sizeof(Node) + h * sizeof(Node*) using a C99 flexible array member (or placement new), keeping the key and its tower pointers in the same 64-byte cache line.

Skip List Multi-Level Express Lane Search & Insertion
Expected O(log N)

Traverse multi-level express towers from top level L_max down to L_0. Move right while next.key < target, otherwise drop down one level — achieving O(log N) search without tree rotations.

Step 1 / 6
Custom Input:
Timeline

Invariant & State

At Level L3, Node(-∞): invariant key(curr) ≤ 42 < key(curr.next[L3])
🔗 Live Pointer & State Transition Graph — Skip List Multi-Level Express Lane Search & Insertion Active Pointer Jump/Transition Matched/Optimal
HEAD [0] ─── [1] ─── [2] ─── [3] ─── [4] 50 [5] ─── [6] ─── [7] HEAD [8] ─── [9]
L3
HEAD
───
───
───
───
50
───
───
L2
HEAD
───
20
───
───
50
───
───
L1
HEAD
───
20
───
42
50
───
75
L0
HEAD
10
20
30
42
50
60
75
Target Key
42
Current Level
L3
Current Node
HEAD (-∞)
Pointer Hops
1
[Step 1] Start at L3 header (-∞). Next at L3 is 50 > 42 → drop to L2.

Production Implementation: Single-Allocation Skip List with $p = 1/4$ Geometric PRNG

#include <array>
#include <cstdint>
#include <cstdlib>
#include <new>

class SkipList {
  static constexpr int MAX_LEVEL = 16; // Supports up to 4^16 = 4.29B elements

  struct Node {
    int key;
    int value;
    int top_level;
    Node* forward[1]; // Flexible trailing array allocated to size top_level + 1
  };

  Node* head_;
  int cur_max_level_ = 0;
  std::uint64_t rng_state_ = 0x853c49e6748fea9bULL;

  static Node* allocate_node(int key, int val, int level) {
    const std::size_t bytes = sizeof(Node) + static_cast<std::size_t>(level) * sizeof(Node*);
    void* raw = ::operator new(bytes);
    Node* node = static_cast<Node*>(raw);
    node->key = key;
    node->value = val;
    node->top_level = level;
    for (int i = 0; i <= level; ++i) node->forward[i] = nullptr;
    return node;
  }

  // Fast xorshift64* PRNG sampling Geometric(p = 1/4) height
  int random_level() noexcept {
    int lvl = 0;
    while (lvl < MAX_LEVEL - 1) {
      rng_state_ ^= rng_state_ >> 12;
      rng_state_ ^= rng_state_ << 25;
      rng_state_ ^= rng_state_ >> 27;
      if (((rng_state_ * 0x2545F4914F6CDD1DULL) >> 62) != 0) break; // 1/4 probability
      ++lvl;
    }
    return lvl;
  }

public:
  SkipList() {
    head_ = allocate_node(-2147483648, 0, MAX_LEVEL - 1);
  }

  const int* find(int target) const noexcept {
    const Node* curr = head_;
    for (int i = cur_max_level_; i >= 0; --i) {
      while (curr->forward[i] != nullptr && curr->forward[i]->key < target) {
        curr = curr->forward[i];
      }
    }
    curr = curr->forward[0];
    if (curr != nullptr && curr->key == target) return &curr->value;
    return nullptr;
  }

  void insert(int key, int value) {
    std::array<Node*, MAX_LEVEL> update{};
    Node* curr = head_;

    for (int i = cur_max_level_; i >= 0; --i) {
      while (curr->forward[i] != nullptr && curr->forward[i]->key < key) {
        curr = curr->forward[i];
      }
      update[i] = curr;
    }

    curr = curr->forward[0];
    if (curr != nullptr && curr->key == key) {
      curr->value = value;
      return;
    }

    const int new_lvl = random_level();
    if (new_lvl > cur_max_level_) {
      for (int i = cur_max_level_ + 1; i <= new_lvl; ++i) {
        update[i] = head_;
      }
      cur_max_level_ = new_lvl;
    }

    Node* inserted = allocate_node(key, value, new_lvl);
    for (int i = 0; i <= new_lvl; ++i) {
      inserted->forward[i] = update[i]->forward[i];
      update[i]->forward[i] = inserted;
    }
  }
};
Production Pitfall: Unbounded MAX_LEVEL & Adversarial Seed Attacks

Never allow random_level() to grow unboundedly without a hard cap MAX_LEVEL (e.g., 16 or 32), or a lucky streak of coin flips on a 10-element list can create a 25-level tower where 20 levels contain only a single node—wasting vertical steps on every query. Furthermore, if you use a fixed deterministic PRNG seed while exposing a Skip List to untrusted external keys, an adversary who knows your PRNG sequence can time insertions so that high towers are assigned to useless keys and level-0 towers are assigned to hot keys; always seed the level generator from hardware entropy (getrandom / std::random_device).

Check yourself Question 1 of 4
Concept Check Skip List Expected Pointer Overhead recall

In Pugh's probabilistic skip list where each node at level $k$ is promoted to level $k+1$ with probability $p$, what is the expected number of forward pointers per node across the entire skip list?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Which memory layout technique merges the skip list node and its variable-height `forward[]` tower into one contiguous block?

Key Takeaways

Skip Lists replace complex deterministic tree rotations with probabilistic multi-level express lanes, achieving O(log N) search, insert, and delete with high probability.
Tracing the search path backwards from level 0 to the top of the head sentinel proves that climbing K levels takes K / p expected steps.
With promotion probability p = 1/4 (used by Redis and RocksDB), the expected number of pointers per node is 1 / (1 - 1/4) = 1.33, using less memory than a binary search tree.
In concurrent systems (RocksDB MemTable, Java ConcurrentSkipListMap), Skip Lists enable lock-free atomic CAS insertions because inserting a node only splices local forward pointers without global tree rotations.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue