Data structures · 14 min
Skip list
Keep a million-player leaderboard in score order and find any score in about 40 steps, using a linked list, some extra lanes and a coin.
Your game has a leaderboard with a million players, kept in score order. Every few milliseconds a match ends and someone’s new score has to slot into the right place.
Finding that place is the hard part. This article gets there in four attempts. The first is obvious and slow; each one after fixes the last one’s problem.
Attempt 1: one long chain of players
A plain sorted array is out. Putting a new score in the middle means shifting every entry after it one slot along, and that can be a million moves.
So use a sorted linked list instead. The new player sakura scores 99. Once you know she goes between mio (96) and kenji (104), adding her is cheap: mio now points to sakura, and sakura points to kenji.
But to find that spot, you start at the lowest score and follow pointers one by one, comparing each score with 99. You can’t jump to the middle, because each item only knows the next one. With a million players that is half a million comparisons on average. Cheap inserts, slow finds.
Attempt 2: add an express lane
A subway line fixes the same problem with an express train. It stops only at some stations. You ride it most of the way, then switch to the local train for the last few stops.
Do the same here. Keep the full chain as level 1. Above it, build level 2: a second chain that links only every other player. To find 99, run along level 2 until the next stop would pass 99, then step down to level 1 and walk the last stretch.
Level 2 holds 500,000 players. On average you walk half of them, 250,000, plus a step or two on level 1. That is half the work of attempt 1. Better, but still far too many.
Attempt 3: lanes on top of lanes
If one express lane halves the work, add another. Level 3 links every other player from level 2, level 4 every other player from level 3, and so on, until the top level holds a single player.
Halve a million 20 times and about 1 player is left, because 2²⁰ is 1,048,576. So there are about 20 express levels above level 1.
A search starts on the top level, on the left, and works down. When it drops to a level, the level above has already brought it to just before the next express stop. On this level that stop is only 2 players away, because the express level kept every other player. So the search looks at no more than 2 scores per level: at most one step right, then one score that is too big. About 20 levels × 2 is about 40 comparisons, instead of half a million.
The wall: someone new joins
Attempt 3 is fast to search, but it depends on a perfect pattern: exactly every other player goes up a level. Now sakura arrives with 99.
Guess first: Take a 12-player board where mio is at position 8. Sakura (99) becomes position 9, just after mio. To keep “every even position is on level 2”, how many of the four players after her have to change their towers?
All four. Kenji was position 9 (not on level 2) and becomes 10 (on it). Akari goes from 10 to 11 (off it). Riku and yuna switch too. Every player after the new one shifts by one position, so every tower after it has to be rebuilt. On a million-player board, a new low score would mean rebuilding about a million towers.
Re-arranging existing items to restore a pattern is called rebalancing. Here it costs as much as the slow search we were trying to avoid. The perfect pattern is the problem.
Attempt 4: let a coin decide
This is the step that makes it a skip list. Drop the rule “every other player”. Instead, each player builds their own tower when they join, by flipping a coin:
- Every player is on level 1.
- Flip a coin. Heads: also join level 2. Tails: stop.
- After each heads, flip again. Each heads adds one more level; the first tails stops it.
- Hook the tower in: on each of its levels, redirect the player before it to point at the new player, and point the new player at the one after.
About half the players get heads on the first flip, so about half reach level 2. Half of those get heads again, so about a quarter reach level 3. That is roughly the shape of attempt 3, but no tower depends on its neighbours.
When sakura joins, she flips her own coins and nobody else changes. When a player leaves, you unhook their tower and nobody else changes either.
Searching works as before: start at the top level on the left, move right while the next score is smaller than the one you want, and drop down a level when it is bigger or the level ends. If the next score is exactly the one you want, on any level, you have found it and can stop.
Joining and leaving, step by step
Every search begins at start: a fixed header before the lowest score, with a slot on every level. Adding sakura (99) is that same search plus a little bookkeeping:
- Search down from the top level. On each level, remember the last player before 99, or start if no one on that level comes before 99. Their pointers are the ones that will change.
- Flip coins to pick her tower’s height. If it is taller than any tower so far, start grows a new, empty level, and start’s slot is what gets remembered there.
- Splice her in, from level 1 upward. On each level of her tower, the remembered player now points to her, and she points to the player it used to point to. That is 2 pointers written per level: one redirected, and one new pointer of her own.
Removing a player runs the same search. The search first meets the leaver on their top level, so from there down you know their tower and remember the player (or start) just before them on each level. On each of those levels, that player now points to whoever the leaver pointed to, skipping over them. The leaver’s own pointers are thrown away with it, so nothing else is written. If the leaver was alone on the top level, that level is now empty, and start drops it.
Guess first: On the demo’s starting board, kaito (90) has a 4-level tower. Remove him. How many pointers change, and how many other towers have to move?
4 pointers, and no towers. One pointer per level: start on level 4, hina on levels 3 and 2, and aoi on level 1 each now point past him. His own pointers don’t need changing, because nothing will follow them again. So a remove costs 1 pointer per level, and an add costs 2.
Watch a search, then change the board
- The box says “find score 96”. Press Play. The search moves right to kaito (90) on level 4, sees kenji (104) is too big, and drops through levels 3 and 2 to find mio (96) on level 1. The counters show 5 scores compared, where a plain list checks 8. Under each name you can see that player’s flips: kaito’s H H H T built a 4-level tower.
- Under “Change the board”, sakura with 99 is filled in. (The coins are a fixed sequence, so if you already changed the board, reload the page first.) Press “Add: flip coins”, then press Next to step through. The search remembers kaito on levels 4 to 2 and mio on level 1 (in purple). Her coins come up heads, then tails: a 2-level tower. The splice changes 2 pointers on each level, 4 in all.
- Pick sakura under “Player to remove” and press “Remove”, then step through: 1 pointer per level, 2 in all. Press “Add: flip coins” again. This time she flips 4 heads and reaches the demo’s cap of 5 levels, so start grows a level 5 before she is spliced in: 10 pointers.
- Remove sakura once more. Once her 5 levels are unlinked, level 5 is empty, so start shrinks back to 4 levels. In every add and remove, the “Other towers moved” counter stays at 0.
Why a million players get about 20 express levels
Follow the coin through a million players. To reach a level, a player needs one more heads in a row than for the level below, so each level keeps about half:
| Level | Heads in a row needed | Chance | Players, out of 1,000,000 |
|---|---|---|---|
| 1 | 0 | every player | 1,000,000 |
| 2 | 1 | 1 in 2 | about 500,000 |
| 3 | 2 | 1 in 4 | about 250,000 |
| 4 | 3 | 1 in 8 | about 125,000 |
| 11 | 10 | 1 in 1,024 | about 977 |
| 21 | 20 | 1 in 1,048,576 | about 1 |
How many scores does a search look at on each level? It is the same gap as in attempt 3, now set by coins. Flip a coin 100 times and you get about 50 heads, so a heads turns up about every 2 flips. In the same way, about every second player on a level also reaches the level above, so the next express stop is about 2 players away. You look at about 2 scores before one is too big and you drop.
Put together: about 2 comparisons on each of about 20 levels is about 40 comparisons for a million players. For n players the rough rule is 2 × log₂ n.
Check it against the demo. 2¹⁶ is 65,536 and 2¹⁷ is 131,072, so log₂ 100,000 is between 16 and 17, about 16.6. The rule gives 2 × 16.6 = 33.2, and the demo measured 35.0. For 1,000 players (log₂ about 10) the rule gives 20 and the demo measured 17.1. Both numbers are in the “At real sizes” table under the demo. The rule is a rough guide, off by a few either way.
Guess first: How much memory do the extra levels cost? Each box in a tower is one pointer. On average, how many pointers does one player need?
About 2. Take 1,000 players: 1,000 boxes on level 1, about 500 on level 2, 250 on level 3, then 125, 63, 31, 16, 8, 4, 2, 1. That adds up to about 2,000 pointers, 2 per player. The demo’s starting board has 12 players and exactly 24 pointers.
The whole thing in code
Search, add and remove share one loop. The part to notice is that it remembers the last player it passed on each level, because those are the pointers add and remove have to change. start is the header from the demo, and top is the tallest tower so far.
find(score):
x = start
for level in top .. 1: # top level first
while x.next[level] and x.next[level].score < score:
x = x.next[level] # move right
before[level] = x # then drop down
x = x.next[1]
return x if x and x.score == score else none
add(player):
find(player.score) # fills before[]
height = 1
while flip_coin() == heads:
height += 1
if height > top: # a new tallest tower
for level in top+1 .. height:
before[level] = start
top = height
for level in 1 .. height: # hook in, level by level
player.next[level] = before[level].next[level]
before[level].next[level] = player
remove(score):
x = find(score) # fills before[]
if x is none: return
for level in x.height .. 1: # unhook, top level first
before[level].next[level] = x.next[level]
while top > 1 and start.next[top] is none:
top -= 1 # drop an empty top levelWhere real leaderboards keep their scores
Redis is an in-memory database that many games use for exactly this job. Its sorted sets keep members ordered by score. Once a sorted set grows past 128 members (the default setting), Redis stores it as a skip list plus a hash table (the structure behind a map).
The hash table answers “what is aoi’s score?” in one lookup. The skip list answers “where does 99 go?” and “who are the top 10?” in a few dozen steps, even with millions of members.
Redis tunes the coin: it comes up heads only 1 time in 4. For 1,000 members that is about 1,000 + 250 + 63 + 16 + 4 + 1 ≈ 1,334 pointers, about 1.33 each instead of 2. Searches take a few more steps, but memory is what an in-memory database runs short of first.
- LevelDB and RocksDB are databases that store data on disk in sorted files. New writes arrive fast and must stay sorted until they are saved, so they collect in an in-memory skip list first.
- Java’s ConcurrentSkipListMap is a sorted map many threads can update at once. An insert touches only the pointers next to its own tower, so two threads adding far-apart scores don’t get in each other’s way.
So when a collection has to stay sorted while things join and leave all the time, a skip list gives you a linked list’s cheap inserts and search in about 2 × log₂ n steps. The only balancing it ever does is a coin flip.
Remember
- A skip list is a sorted linked list with extra express levels on top.
- Each new item flips a coin to pick its own height, so nothing else is ever rebalanced. An add changes 2 pointers per level of its tower, a remove 1.
- Each level holds about half the items of the one below: about log₂ n levels, about 2 comparisons per level.
- A million items: about 40 comparisons per search and about 2 pointers per item.
- Redis sorted sets use one for leaderboards and rankings.
See it in a design
- Top-K & leaderboards: Exact leaderboards in Redis sorted sets and approximate trending lists from a count-min sketch.
- Key-value store: Dynamo-style: consistent hashing, three replicas, quorums, hinted handoff and Merkle-tree repair.
Further reading and videos
- Skip Lists EXPLAINED | Insertion and Deletion (Tyler AI)
- Read a paper: Skip Lists (Vivek Haldar)
- Skip List | Efficient Search in Sorted Linked List (Fit Coder)
- 7. Randomization: Skip Lists (MIT OpenCourseWare)
- Redis sorted sets (Redis docs)
- Redis sorted set source (t_zset.c): the skip list, “almost a C translation” of Pugh’s (Redis on GitHub)
- LevelDB’s skip list (db/skiplist.h) (Google on GitHub)
- ConcurrentSkipListMap (Java SE 21) (Oracle)
- Skip list (Wikipedia)
- Skip Lists: A Probabilistic Alternative to Balanced Trees (1990) (William Pugh, CACM)