Skip Lists & Probabilistic Structures
Balanced trees achieve with rotations and invariants; randomization gives the same bound far more simply. A skip list is a layered linked list whose express lanes are chosen by coin flips, giving expected search and insert with no rebalancing.
╌╌╌╌
A balanced search tree earns its guarantee with machinery: height invariants, rotations, and fix-up cases you have to get exactly right. This lesson takes a different route to the same bound: randomization. Instead of forcing balance with invariants, we expect it from coin flips, and the resulting structures are far simpler to implement. This lesson covers two. The skip list is a sorted dictionary that matches a balanced tree's bounds with nothing but linked lists and a random number generator. The Bloom filter gives up exactness entirely: it answers set membership in a few bits per element, accepting a controlled rate of false positives in exchange for tiny space.
Skip lists: express lanes by coin flip
Start with an ordinary sorted linked list. Search is because you must walk node by node; there is no way to skip ahead. To speed this up, add express lanes: a sparse second list linking every other node, then a sparser third linking every fourth, and so on. A search rides the top express lane until it would overshoot, drops down a level, and repeats. With lanes each twice as sparse as the one below, a search takes steps — the same idea as a balanced tree, built from lists.
The catch is keeping those lanes perfectly spaced under insertion and deletion, which would cost as much as rebalancing a tree. The skip list avoids this: instead of maintaining exact spacing, it assigns each node a random height by flipping a coin.1 Every new node enters level . Then flip a fair coin: heads promotes the node into level ; another heads promotes it into level ; the tower stops growing at the first tails. A node therefore reaches level with probability : every node sits in level , about half rise to level , a quarter to level , an eighth to level . On average the lanes are spaced just like the deterministic version, but no rebalancing is ever needed.
Search. Begin at the head of the top level. Move right while the next key is the target; when the next key would overshoot, drop down one level and continue. You reach level at the target (if present) or its predecessor. Each rightward step and each drop is .
- 1top-level sentinel
- 2for down to dowalk levels top to bottom
- 3while and do
- 4ride this lane right while it stays
- 5return if else not found
Trace on the list above. Start at on level . The next key, , is : advance to . The next key on level is : drop to level 's copy of . On level the next key is again : drop to level . There the next key is : advance, and : found after two rightward moves and two drops, where the plain list would have walked four nodes. Searching for an absent key, say , follows the identical path but stops at on level (since ) and reports not-found. The search lands on the predecessor of the missing key — the node an insertion needs.
Insert: search, flip, splice
Insertion is a search with bookkeeping, followed by coin flips, followed by pointer surgery. Three phases:
- Search and remember. Walk the usual search path for the new key , but at each level record the last node visited before dropping: the update vector . Node is 's predecessor on level : the node whose level- pointer must be redirected if the new tower reaches level .
- Flip for a height. Set and flip until tails, incrementing on each heads.
- Splice. For each level : the new node's level- pointer takes over 's old target, and 's level- pointer is redirected to the new node. Two assignments per level. If exceeds the current height, the new levels start at the head sentinel.
- 1
- 2for down to do
- 3while and do
- 4
- 5predecessor of on level
- 6
- 7while dogeometric height
- 8create node with key and height
- 9for to dosplice, bottom level up
- 10
- 11
Run on the running example. The search path is the one already traced for : on level , advance , see , record , drop; on level , see , record , drop; on level , see , record . The update vector is the tower of at every level; the new key falls in the gap between and .
Suppose the flips come up heads, then tails: the tower stops at , so joins levels and only. The splice is four pointer assignments. On level : 's pointer takes 's old target , and now points to . On level : 's pointer takes the level- copy of 's old target , and that copy now points to . Level is untouched because the tower never reached it. Nothing else in the list moves.
Delete is the mirror image, with no coins at all. Search for the key with the same bookkeeping, so is the node before the victim on each level; then, for every level the victim occupies, redirect past it to the victim's own level- successor. Deleting from the list above reverses the four assignments of the insert and restores the original picture exactly. Neither operation rebalances anything; the random heights keep the structure well-spaced in expectation, whatever the order of insertions and deletions.
Heights are geometric
The height assigned to a node is plus the number of consecutive heads, so
a geometric distribution. Two consequences follow immediately. First, space: the number of pointers a node carries equals its height, and
so a skip list stores about pointers in expectation, the same order as the child pointers of a binary tree. The express lanes cost only one extra pointer per node on average.
Second, height of the whole list, which is the maximum of independent geometrics. That maximum concentrates tightly around , and the next two results make the guarantee precise.
Why the expected height and search cost are
The whole guarantee rests on two facts about the coin flips.
So search, insert, and delete are all expected , matching a balanced tree — but the code is a few dozen lines with no rotation cases, and the bound holds in expectation over the random heights, independent of the input order.2 The result is a balanced-tree guarantee from a coin and a linked list.3 Skip lists also parallelize and support concurrent updates more gracefully than rotation-based trees, which is why several production key-value stores use them.
Bloom filters: membership in a handful of bits
A skip list still stores every key. Sometimes that is too expensive: with a billion URLs, the only question may be whether a given URL has been seen before, and storing the URLs themselves is out of the question. A Bloom filter answers that membership question in a tiny, fixed amount of space, by giving up the ability to answer it exactly.
The structure is a bit array , initially all , plus independent hash functions , each mapping a key to a position in .
- Insert : set the bits to .
- Query : report present only if all bits are ; otherwise report absent.
The two answers are not symmetric. If even one of 's bits is , then
was never inserted (inserting would have set it), so a absent
answer is
always correct — a Bloom filter has no false negatives. But a present
answer can be wrong: the bits for some never-inserted might all have been
set to by other elements' insertions, a false positive.
This is precisely the soundness / completeness framing from the foundations.
Read as a test for is not in the set,
the filter is sound:
whenever it says absent
it is right. Read as a test for is in the set,
it is incomplete: present
may be a false alarm. A Bloom filter is a
definitely-not-present oracle: a negative answer is a proof, a positive answer
is only a strong hint.
The false-positive rate
How often is present
wrong? Inserting elements evaluates hashes, each
choosing a position uniformly in . A fixed bit survives as only if
every one of those throws misses it:
using for large . So after the insertions a fraction of the array is set. A false positive needs all of a query's probes to land on set bits, and treating the probes as independent hits on that fraction gives the false-positive probability
Two effects compete in the formula. More hashes make each query harder to pass by accident (the exponent grows), but they also fill the array faster (the base grows). Somewhere in between is a best .
Concrete numbers. Suppose you track URLs with bits, a MB array and bits per element. The optimum is ; take . Then , each bit ends up set with probability , almost exactly half as the theorem predicts, and
about false alarms per queries of absent keys. Doubling the budget to bits per element drives the rate to , under one in ten thousand. A hash set storing the URLs themselves would need hundreds of bits per element before a single pointer is counted.
The practical reading: the rate falls exponentially in the bits-per-element. About bits per element with hashes gives a false-positive rate; bits and gives — orders of magnitude less than the dozens of bytes per element a hash set storing the keys would need.
Bloom filters are everywhere a cheap, conservative pre-check pays off: a database
skips a disk lookup when the filter says a key is absent; a CDN avoids caching a
one-hit URL; a spell-checker rejects obvious non-words. In each case the filter's
no-false-negative guarantee is what makes it safe — a definitely absent
answer can be trusted to short-circuit the expensive exact check, and a maybe present
answer simply falls through to that check.
Where the coin flips run
Both structures are core pieces of widely-used systems, and each has refined descendants.
Skip lists in production. Redis stores its sorted sets (ZSET) as a skip
list paired with a hash table, precisely because a skip list gives
ordered operations and range queries with far simpler concurrent code than a
balanced tree: there are no rotations to coordinate, so lock-free and
fine-grained-locking skip lists are practical, which is why concurrent maps
(Java's ConcurrentSkipListMap) favor them. The MemSQL/LevelDB-style in-memory
memtable is often a skip list for the same reason.
Bloom filters and the LSM tree. The dominant use of Bloom filters today is
inside log-structured merge trees (LevelDB, RocksDB, Cassandra): each on-disk
table carries a Bloom filter, so a read that would otherwise probe many tables
skips the ones whose filter says absent,
turning most negative lookups into a
few in-memory bit tests. This is the skip a disk lookup
case at industrial
scale.
Filters past Bloom. The no-deletion limitation and Bloom's cache-unfriendly
scattered probes drove better designs. A counting Bloom filter restores
deletion; a cuckoo filter (Fan et al., 2014) stores small fingerprints in a
cuckoo-hash table, supporting deletion and better locality at the same false-
positive rate; a quotient filter is a cache-friendly, mergeable alternative.
And for the related question how many distinct items,
the same probabilistic
spirit gives HyperLogLog, taken up in
Data-Stream Algorithms.4
Takeaways
- Randomization reaches a balanced tree's bounds without invariants or rotations — expected balance from coin flips, far simpler to implement.
- A skip list layers sorted linked lists; each element rises to the next level with probability . Search rides express lanes top-down, dropping a level on overshoot.
- Insert is a search that records the update vector (the predecessor on each level), a run of coin flips for the height, then two pointer assignments per level. Delete reverses the splice. No rebalancing, ever.
- Tower heights are geometric: expected height , so about pointers in total: one extra pointer per node over a plain list.
- The height is with high probability, and search/insert/delete are all expected , independent of input order.
- A Bloom filter is a bit array plus hashes. Insert sets bits; query reports present only if all are set.
- It has no false negatives — a sound
definitely-not-present
test — but is incomplete as a presence test, with false-positive rate , minimized at . - The rate drops exponentially in bits-per-element; the cost is that you cannot delete (without a counting variant).
Footnotes
- Skiena, §3.x — Randomized Data Structures: skip lists as a coin-flip alternative to balanced trees with expected operations. ↩
- Erickson, Ch. — Randomized Algorithms: analysis of randomized structures, including expectation over internal coin flips rather than over inputs. ↩
- CLRS, App. C — Counting and Probability: the union-bound and geometric-variable arguments behind the expected height and search cost. ↩
- Pugh,
Skip lists: a probabilistic alternative to balanced trees
(1990); Fan, Andersen, Kaminsky & Mitzenmacher,Cuckoo filter: practically better than Bloom
(2014); the LSM-tree Bloom-filter pattern is standard in LevelDB/RocksDB. ↩
╌╌ END ╌╌