Data structures and algorithms path · Structures in production · Topic 13 of 13

Tries and Bloom filters

Not started

Some of the most useful data structures in production are not the ones in the first chapter of a textbook. A trie stores strings as paths of characters, so all keys with a common prefix share the same branch, which makes prefix search and longest-prefix matching cheap. A Bloom filter is a compact bit array that can tell you an item is definitely not in a set, using a few bits per item, at the price of sometimes saying "maybe" when the answer is no.

Why it matters: Autocomplete, URL routers, IP lookup tables and every LSM-tree database (RocksDB, Cassandra) lean on these two less famous structures.

  1. 1A trie branches on characters

    Each edge is one character, so all words sharing a prefix share a path. Looking up a key costs its length, not the number of keys.

  2. 2Prefix queries for free

    Walk down the prefix, then everything below that node starts with it. Hash tables can't do that.

  3. 3A Bloom filter answers maybe or no

    k hash functions set k bits per item in a bit array. If any of an item's bits is 0 it was never added; if all are 1 it probably was.

  4. 4Tune the false-positive rate

    About 9.6 bits per item and 7 hash functions give roughly a 1% false-positive rate, whatever the size of the items.

1

How it works

The explanation, with examples you can run.

Tries

A trie's root represents the empty string. Each node has a child per next character, and nodes that complete a word are marked. Looking up a key takes one step per character, independent of how many keys are stored, and finding every key with a given prefix means walking to the prefix's node and listing everything below it:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

That makes tries natural for autocomplete, spell checkers and routing tables. A one-character-per-node trie wastes a lot of memory on long chains with a single child, so real implementations compress those chains into one edge labelled with a whole substring. That is a radix tree (or Patricia trie), the structure many HTTP routers use to match paths like /users/:id/posts, and the basis of longest-prefix matching for IP routes.

Bloom filters

A Bloom filter is an array of m bits, all 0 at the start, plus k hash functions that each map an item to a bit position. Adding an item sets its k bits. Checking an item looks at the same k bits: if any is 0, the item was definitely never added; if all are 1, it was probably added, but other items may have set those bits by coincidence. There are no false negatives, only false positives, and the filter never stores the items themselves.

For n items, the false-positive rate is about (1 − e^(−kn/m))^k, and the best k is (m/n) · ln 2. Roughly 9.6 bits per item with 7 hash functions gives 1%, and every extra 4.8 bits per item divides the rate by ten, regardless of whether the items are 8-byte ids or 2 KB URLs. The example uses one well-mixed hash and derives the k positions from two values, a trick from Kirsch and Mitzenmacher that loses nothing in accuracy:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

The measured rate matches the formula, 1.00%. Without the final mixing step the bit positions of similar keys like user-1 and user-2 were correlated: fewer bits got set than the formula assumes, and the measured rate came out at 0.54% instead of 1.00%. Better than predicted, but no longer predictable, which is a good reminder that a Bloom filter is only as good as its hash.

Bloom filters earn their place in front of something expensive. An LSM-tree database such as RocksDB or Cassandra keeps one per on-disk file, so a read for a key that isn't in a file usually skips reading that file at all. Caches use them to avoid storing items seen only once, and distributed joins use them to avoid shipping rows that can't match. Removing items needs a variant, such as a counting Bloom filter or a cuckoo filter, because clearing a shared bit would create false negatives.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A Bloom filter says an item is not in the set. How sure can you be?
  2. 2.Why can't you delete from a standard Bloom filter by clearing an item's bits?
  3. 3.What does a trie lookup for a word of length L cost in a trie holding n words?

0 of 3 right so far. Get all three to finish this topic.