AnyLearn
All lessons
Computer Scienceintermediate

Tries and Radix Trees: Structures Keyed by Prefix

A trie stores keys in their spelling rather than hashing them, which buys the one query a hash table cannot answer: find everything starting with this. This lesson covers the trie, the memory problem that makes it impractical, and the radix compression that fixes it and routes the internet.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

The query nothing else answers

A hash table finds an exact key fast. A balanced tree finds a key and its neighbours in sorted order. Neither answers this well: give me every key beginning with "rec".

A hash table cannot, because hashing deliberately destroys the relationship between similar keys, and "recall" and "receipt" land in unrelated slots. A sorted tree can, since shared prefixes sort adjacently, but every comparison costs a full string comparison, and it cannot answer the routing variant of the question: which of my stored prefixes is the longest one matching this key?

A trie answers both, because it does something neither of the others does. It stores the key in the shape of the structure itself, one character per level, so a prefix is not something you search for. It is a location.

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. The query nothing else answers

A hash table finds an exact key fast. A balanced tree finds a key and its neighbours in sorted order. Neither answers this well: give me every key beginning with "rec".

A hash table cannot, because hashing deliberately destroys the relationship between similar keys, and "recall" and "receipt" land in unrelated slots. A sorted tree can, since shared prefixes sort adjacently, but every comparison costs a full string comparison, and it cannot answer the routing variant of the question: which of my stored prefixes is the longest one matching this key?

A trie answers both, because it does something neither of the others does. It stores the key in the shape of the structure itself, one character per level, so a prefix is not something you search for. It is a location.

2. The key is the path

In a trie, no node stores a whole key. Each edge carries one character, and a key is the concatenation of characters along the path from the root.

Storing car, cat, card and cow gives the shape above. The words car, cat and card share the path c-a, so that path exists once and is walked by all three. A node is marked as terminal to record that a key ends there, which is how card can extend car without either being lost.

Two consequences follow immediately. Lookup costs the length of the key and is completely independent of how many keys are stored, because you never compare against other entries. And every key sharing a prefix lives in one subtree, so prefix search is: walk to that node, then collect everything beneath it.

flowchart TD
R["root"] --> C["c"]
C --> A["a"]
A --> R2["r (end: car)"]
A --> T["t (end: cat)"]
R2 --> D["d (end: card)"]
C --> O["o"]
O --> W["w (end: cow)"]

3. Lookup that ignores the collection size

This independence from n is the trie's distinctive property, and worth stating carefully.

A hash table lookup is constant on average but must compute a hash over the whole key and then compare the full key to confirm a match. A balanced tree does about log n comparisons, each of which may compare long strings. A trie walks one node per character: at most as many steps as the key is long, whether the trie holds a thousand keys or a billion.

The catch is that a step is a child lookup, and how you store children decides everything. An array of 26 pointers per node is a single index, and enormous. A hash map per node is compact and slower. A sorted array of present children is a small binary search and cache-friendly. Real implementations agonise over this choice.

4. The memory problem

The naive trie is often unusable, and the reason is chains of single-child nodes.

Store the single key "internationalization". That is 20 characters, so the trie allocates 20 nodes, each holding one character and a child pointer table, and 19 of them have exactly one child. You have spent 20 node allocations to represent one string that a hash table would store in one.

The waste is worst exactly where you would want a trie: long keys with few shared prefixes. A node with a 256-entry pointer array for byte keys costs a kilobyte or two on a 64-bit machine, and almost all of it is null. This is the point at which most people conclude tries are a textbook curiosity.

5. Radix compression

The fix is direct. If a node has exactly one child, it carries no information: there is no decision to make there. So merge it into its child.

A radix tree, also called a compressed trie or Patricia trie, stores a string fragment on each edge rather than a single character. The chain for "internationalization" collapses from 20 nodes into one node holding the whole string. Nodes now exist only where keys actually diverge, so the node count is bounded by the number of keys rather than by their total length.

The savings are large in practice. Redis's rax implementation stores compressed prefixes inline in the node struct, and its documentation reports the technique cutting memory several-fold for string sets with long shared prefixes. Lookup logic barely changes: instead of matching one character per node, you match a fragment.

6. Longest prefix match, and why routers need it

IP routing cannot use a hash table, and the reason is a different question shape.

A routing table does not store destinations, it stores network prefixes such as 10.0.0.0/8 and 10.1.0.0/16. An arriving packet for 10.1.2.3 matches both, and the rule is that the most specific one wins: the longest prefix match. A hash table can only answer whether an exact key is present, so it cannot express "most specific match" at all without probing every possible prefix length.

A trie answers it by construction. Walk the destination address bit by bit from the root, remembering the last node marked as a route. When you can walk no further, that remembered node is the longest matching prefix. The Linux kernel uses a compressed trie for exactly this, and BGP routers apply the same structure to IPv4 tables that now exceed a million entries.

7. Where else they turn up

Once you recognise the shape, radix trees appear in a lot of infrastructure.

Autocomplete is the obvious one: the user's typed characters are a path, and the candidate completions are the subtree below it. Ordering the results then needs a separate signal, usually a score stored at the terminal nodes.

Redis Streams index entries by a radix tree keyed on time-ordered ids stored big-endian, so nearby entries share long prefixes and compress well, which is precisely the case radix trees handle best. HTTP routers match URL paths, which are prefix-structured by design. Filesystems such as Btrfs index blocks this way. Ethereum's state is a Merkle Patricia trie, a radix tree in which every node is a hash of its children, so the same structure that locates a key also proves its value.

8. Choosing between the three structures

You now have the full set from this path, and they separate cleanly by the question being asked.

QuestionStructure
Is this exact key present?Hash table
What are the keys between x and y, in order?Balanced search tree
What keys start with this prefix?Trie or radix tree
Which stored prefix best matches this key?Radix tree
What is the smallest remaining item?Heap

The honest caveat is that a trie is a specialist. For exact lookup a hash table is faster and simpler, and a radix tree carries more pointer indirection and worse locality than a flat table. Reach for one when the prefix relationship is genuinely part of the problem, not merely because the keys happen to be strings.

9. Practical notes

Few languages ship a trie in the standard library, so you will use a package or write one. A minimal version is short:

def insert(root, word):
    node = root
    for ch in word:
        node = node.setdefault(ch, {})
    node["$"] = True          # terminal marker

def starts_with(root, prefix):
    node = root
    for ch in prefix:
        if ch not in node:
            return None
        node = node[ch]
    return node               # subtree of all completions

Three things to watch. Decide your alphabet first: bytes are simpler and Unicode-safe, characters are more intuitive but explode the branching factor. Use a terminal marker that cannot collide with a real symbol. And if the key set is static, build a compact immutable form once rather than a pointer structure, since the write path is where tries are most wasteful.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why can a hash table not answer prefix queries?
    • Because hashing deliberately scatters similar keys into unrelated slots
    • Because hash tables cannot store string keys
    • Because hash tables have no way to iterate their contents
    • Because prefix queries require sorted input
  2. What makes trie lookup independent of the number of stored keys?
    • The trie is rebalanced after every insertion
    • The key is spelled out along the path, so lookup costs the key's length, not a comparison against other entries
    • Every node stores a hash of the remaining key
    • The trie keeps all keys in one sorted array
  3. What problem does radix compression solve?
    • It sorts the keys stored in the trie
    • It removes the need for terminal markers
    • It allows duplicate keys to be stored
    • It collapses chains of single-child nodes, which carry no decision and waste an allocation each
  4. Why does IP routing require a trie rather than a hash table?
    • Because routing tables are too large for hash tables
    • Because IP addresses cannot be hashed
    • Because routing needs the longest matching prefix, which an exact-key structure cannot express
    • Because routing tables must be kept in sorted order
  5. When is a trie the wrong choice despite string keys?
    • When the workload is exact-key lookup, where a hash table is faster and has better locality
    • Whenever keys are longer than ten characters
    • Whenever the key set changes over time
    • When keys share no common prefix at all

Related lessons

Computer Science
advanced

Count-Min, and Why Sketches Compose

The count-min sketch estimates how often an item occurred using a fixed grid of counters, and it never underestimates. This lesson builds it, states the error bound that makes it usable, shows where it is useless, and ends on the property shared by all three structures that explains why they run distributed systems.

8 steps·~12 min
Computer Science
advanced

HyperLogLog: Counting Distinct Items in Kilobytes

Counting distinct items exactly needs memory proportional to the count. HyperLogLog answers the same question in a fixed twelve kilobytes, for cardinalities into the billions, by measuring an improbable event rather than storing anything. This lesson builds that idea from the leading-zeros intuition up.

8 steps·~12 min
Computer Science
advanced

Bloom Filters: Membership in a Bit Array

A Bloom filter answers set membership using a bit array and a handful of hash functions, with no items stored anywhere. This lesson builds it, derives the sizing formula that trades memory against false positives, explains exactly why deletion is impossible, and covers the variants that buy it back.

8 steps·~12 min
Computer Science
intermediate

The Bargain: Bounded Memory for Bounded Error

Answering set questions exactly costs memory proportional to the data, which fails once the data does not fit. Probabilistic data structures accept a quantified error in exchange for memory that stays constant. This lesson establishes what that trade buys, and why the shape of the error matters more than its size.

8 steps·~12 min