SysDesignPrep.com
Interactive concepts

Skip list: an interactive visualization of search and express lanes

A skip list is a sorted linked list with express lanes. Every key is on the bottom level; about half are also on the level above, a quarter on the level above that, and so on, decided by coin flips. A search runs along the sparse top lane and drops down as it gets close, so finding, inserting and deleting take O(log n) steps on average, with no rebalancing. Redis sorted sets, the memtables of LevelDB and RocksDB, and Java’s ConcurrentSkipListMap are skip lists.

The playground draws a skip list as a grid, keys across and levels up. Pick how many keys and how likely a promotion is, flip the coins again, and click a key on the bottom level to search for it.

KeysPromote with probability

16 keys in sorted order on level 1. Each key is promoted to the next level up with probability 1/2, as if by flipping a coin, so this list is 5 levels tall with 2.3 forward pointers per key. Searching for 80 takes 7 comparisons, moving right along the express lanes while the next key is smaller and dropping a level when it is not; walking the linked list from the start takes 14. Averaged over every key: 7.5 comparisons against 8.5. The gap widens with size: at a million keys a search takes about 40 comparisons; the linked list, 500,000.

L54852endL448527584endL34852567584endL2584852565773758084endL1end
This search7comparisons; 14 in a linked list
Average search7.5comparisons; 8.5 in a linked list
Height5 levelstallest key
Pointers per key2.3expected 2.0
where the search stoodcompared, too big: drop a levelfoundClick a key on the bottom level to search for it.

Search and insert

A search starts at the top of the head and moves right while the next key on that lane is smaller than the target; when the next key is too big, or the lane ends, it drops a level. On the bottom level the next key is the target, or the target is absent. An insert does the same walk, remembering the last node it passed on each level, flips coins to choose the new key’s height, and splices it into those levels. Only the neighbouring pointers change, which is why skip lists are a favourite for concurrent, lock-free sorted maps.

Why coin flips are enough

With promotion probability 1/2, the expected height is about log2 n and a search makes about 2 log2 n comparisons: around 40 for a million keys, against 500,000 for walking a list. Each key carries 2 forward pointers on average. With probability 1/4 the expected search is about the same, since lanes are sparser but there are fewer of them, and keys carry only 1.33 pointers, which is why Redis uses 1/4. Bad luck is possible but rare: the chance of a search much slower than average falls off exponentially.

On small lists the express lanes do not pay: in the playground, 32 keys average 8.9 comparisons against 16.5 for the linked list, but 8 keys can take more comparisons than walking them. Redis stores small sorted sets, up to 128 entries by default, as a flat packed list and switches to a skip list beyond that.

A skip list insert in code

import random

class Node:
    def __init__(self, key, level):
        self.key, self.next = key, [None] * level

def insert(head, key, p=0.5):
    """head is a Node with a pointer for every possible level."""
    update, node = [], head
    for lvl in reversed(range(len(head.next))):       # top lane down
        while node.next[lvl] and node.next[lvl].key < key:
            node = node.next[lvl]
        update.append(node)                            # last node before key, per lane
    update.reverse()
    level = 1
    while random.random() < p and level < len(head.next):
        level += 1                                     # flip coins for the height
    new = Node(key, level)
    for lvl in range(level):                           # splice in: only local pointers change
        new.next[lvl], update[lvl].next[lvl] = update[lvl].next[lvl], new

Redis sorted sets put this behind commands. Each lane pointer also stores how many keys it skips, so rank queries are O(log n) too:

ZADD leaderboard 4200 alice 3900 bob 5100 carol
ZINCRBY leaderboard 300 bob                 # O(log n): find, unlink, reinsert
ZRANGE leaderboard 0 9 REV WITHSCORES       # top 10: a walk along level 1
ZREVRANK leaderboard bob                    # O(log n), from spans kept on the lanes

In a system design interview

Skip lists come up through Redis: “The leaderboard is a Redis sorted set, a skip list plus a hash from member to score, so a score update and a rank lookup are both O(log n), and the top 100 is a short walk along the bottom level. Ten million players fit in a few gigabytes on one shard; beyond that we shard by score range or keep exact ranks only for the top.” Expect the follow-ups: why not a B-tree in a database, how to rank across shards, and how ties are ordered.

Read the guide to skip lists and sorted sets or Redis data structures, compare the B-tree and the LSM tree, whose memtable is often a skip list, or watch a strong candidate design a leaderboard.

Questions people ask

What is a skip list?

A sorted linked list with extra levels of forward pointers. Every element is on the bottom level, and each is promoted to the next level with a fixed probability, so higher levels act as express lanes. Search, insert and delete take O(log n) expected time without any rebalancing.

How does searching a skip list work?

Start at the top level of the head node. Move right while the next element on that level is smaller than the target; when it is larger or the level ends, drop down a level. On the bottom level the next element is either the target or proof that the target is absent.

Why does Redis use a skip list for sorted sets?

A skip list gives O(log n) inserts, deletes and range walks, is simple to implement, and can store in each pointer how many elements it skips, which makes rank queries O(log n). Redis pairs it with a hash table from member to score for O(1) score lookups, and uses a compact list for small sets.

What probability should a skip list use?

One half and one quarter both give about 2 log2 n expected comparisons per search. One quarter uses fewer pointers, about 1.33 per element instead of 2, which is why Redis uses it. The maximum level is capped, at 32 in Redis, which is plenty for billions of elements.

Skip list vs balanced tree: which is better?

Both give O(log n) operations. Balanced trees guarantee it in the worst case; skip lists give it with high probability. Skip lists are simpler, need no rotations, and make concurrent lock-free implementations easier, which is why they appear in Redis, LevelDB, RocksDB and Java’s ConcurrentSkipListMap.

What is the space overhead of a skip list?

Each element stores one forward pointer per level it reaches. With promotion probability p that is 1 / (1 - p) pointers on average: 2 for p = 1/2 and about 1.33 for p = 1/4, plus the key and value themselves.

Where are skip lists used in databases?

LevelDB and RocksDB use a skip list as the in-memory memtable that buffers writes before they are flushed to sorted files, because it stays sorted under concurrent inserts. Redis uses one for sorted sets, and Lucene uses skip lists inside posting lists to jump ahead during searches.