1. Introduction & Motivation
The standard trie stores one character per node in a hashmap-of-children structure. That design is easy to reason about, but it has real costs: a long chain of single-child nodes wastes both memory and traversal steps, a hashmap per node carries overhead that dwarfs the single byte it indexes, and the character-per-node model does not fit numeric data such as fixed-width integers at all.
Four variants fix different parts of that picture. The compressed trie (radix tree) collapses single-child chains into single edges, cutting node count from one-per-character to roughly one-per-branch-point. The Patricia trie pushes that same idea to the bit level, which is exactly what routers need for longest-prefix-match lookups. The binary trie repurposes the same branching idea for fixed-width binary numbers, unlocking greedy algorithms such as maximum XOR pair. The ternary search trie keeps the memory profile of a binary search tree while retaining prefix-search power, avoiding both the wide array and the hashmap. Finally, the persistent trie shows how any of these structures can keep every past version accessible almost for free.
2. Prerequisites
This lesson builds directly on the Union-Find, Trie, Fenwick & Segment Tree guide, and assumes the standard hash-map-children trie — its insert, search, and starts_with operations, and its $O(L)$ per-operation cost for a key of length $L$ — is already understood. That lesson is not repeated here; every section below assumes you can already build and reason about the plain trie, and instead focuses on what changes structurally in each variant.
3. Compressed Trie / Radix Tree
Inserting "test", "team", "toast"
Start from an empty radix tree and insert three words that share prefixes at different depths. Each insert either extends the tree or splits an existing edge at the point where the new key diverges from what is already stored.
| Step | Insert | Existing edge examined | Common prefix | Action |
|---|---|---|---|---|
| 1 | test | (none — first key) | — | Create edge "test" from root to a new leaf, marked end-of-word. |
| 2 | team | "test" | "te" (length 2 of 4) | Split "test" at position 2: new internal node under edge "te", with children "st" (old leaf, test) and "am" (new leaf, team). |
| 3 | toast | "te" | "t" (length 1 of 2) | Split "te" at position 1: new internal node under edge "t", with children "e" (leading to the test/team split) and "oast" (new leaf, toast). |
The final tree has five nodes total for three words of combined length 13 — a plain trie would need 13 single-character nodes for the same keys (fewer once chains merge, but still far more than five). Every node in the radix tree exists because two or more keys actually disagree there.
Below, each edge is stored as a (label, child) pair keyed by its first character, so a node can still find the right child in $O(1)$ expected time. Inserting compares the new suffix against the stored edge label one character at a time to find the longest common prefix, then either descends past a fully-matched edge or splits a partially-matched one.
Patricia Trie
A Patricia trie (Practical Algorithm To Retrieve Information Coded In Alphanumeric) is the same collapsing idea taken to the bit level instead of the byte or character level. Where the radix tree above stores a substring on each edge, a Patricia trie stores a bit-string span, and each node keeps a skip count — the number of bits to skip before the next branching decision — instead of the literal bits themselves. That skip count is enough because the bits being skipped are implied by which branch was taken to reach the node.
This is exactly the structure real routers use for IP routing tables: an IPv4 or IPv6 address is a fixed-width bit string, and a Patricia trie over the address space supports longest-prefix-match lookups — find the most specific CIDR block that contains a given address — in time proportional to the address width, not the number of routes installed. Conceptually:
// Patricia node (conceptual, not implemented here):
// struct PatriciaNode {
// int skip; // bits to skip before testing the branch bit
// PatriciaNode* left; // branch bit is 0
// PatriciaNode* right; // branch bit is 1
// };
This lesson does not build a full Patricia trie — it is a conceptual bridge between the byte-level radix tree above and the bit-level binary trie below. The binary trie in the next section is the simplified, uncompressed version of the same bit-oriented idea, chosen because it is what the classic maximum-XOR interview problem is built on.
4. Binary Trie for XOR (Maximum XOR Pair)
This greedy walk solves Maximum XOR of Two Numbers in an Array (LeetCode 421): insert every number into the trie, then for each number query the trie for its best XOR partner, tracking the overall maximum.
Tracing the greedy query for every number in the sample array (5-bit width shown for readability; the code above uses 32 bits):
| Number | Binary | Best partner | Partner binary | XOR result |
|---|---|---|---|---|
| 3 | 00011 | 25 | 11001 | 26 |
| 10 | 01010 | 25 | 11001 | 19 |
| 5 | 00101 | 25 | 11001 | 28 |
| 25 | 11001 | 5 | 00101 | 28 |
| 2 | 00010 | 25 | 11001 | 27 |
| 8 | 01000 | 25 | 11001 | 17 |
The overall maximum across every row is $28$, from the pair $(5, 25)$ — exactly what the code prints. Notice that 25 shows up as the best partner for four of the six numbers: its high bits (11) are rare in this array, so it differs early and often, which is precisely what the greedy walk is built to reward.
5. Ternary Search Trie (TST)
Insertion recurses on the current character: go left if it's smaller than the node's character, right if larger, or advance to the next character along the equal link if it matches. Prefix collection is an inorder-style traversal that visits left, then the node itself, then recurses into equal (appending the node's character to the prefix), then right — which yields results in ascending lexicographic order for free.
6. Persistent Trie
Every trie variant above mutates in place: inserting a word changes the tree, and the previous version is gone. A persistent trie keeps every version reachable instead, using a technique called path copying. When a word is inserted, only the nodes that lie on the path from the root to the modified leaf are cloned; every other subtree is left untouched and simply shared, by pointer, between the old version and the new one.
Because a trie's path length for one key is bounded by the key's length (or bit width, for a binary trie), path copying touches only $O(L)$ nodes per update no matter how large the overall structure is — the new version costs a small, bounded amount of extra memory rather than a full copy. The old root pointer still describes the tree exactly as it was before the insert, so both versions can be queried independently and forever.
Persistence as a general technique — applying path copying to balanced trees, arrays, and other structures, along with fat nodes and other space-saving refinements — is broad enough to deserve its own treatment. This lesson only introduces the idea as it applies to a trie; a dedicated Persistent Trees lesson in a later part of the curriculum covers the general technique in depth.
7. Complexity Comparison Table
| Structure | Space | Insert | Search |
|---|---|---|---|
| Standard trie (hashmap children) | $O(N \cdot L)$ nodes, high per-node overhead | $O(L)$ | $O(L)$ |
| Compressed trie / radix tree | $O(n)$ nodes ($\le 2n-1$) | $O(L)$ amortized | $O(L)$ |
| Binary trie (fixed $B$-bit keys) | $O(n \cdot B)$ nodes | $O(B)$ | $O(B)$ |
| Ternary search trie | $O(n \cdot L)$ nodes, 3 pointers each (low overhead) | $O(L)$ average | $O(L)$ average |
Here $n$ is the number of stored keys, $L$ is average key length, $N \cdot L$ is total character count across all keys, and $B$ is the fixed bit width (32 for a typical integer). The radix tree wins on node count; the ternary search trie wins on per-node overhead; the binary trie is the only one shaped for numeric greedy algorithms like maximum XOR.
8. Applications & Alternatives
Where each variant is the right default
- Patricia tries: IP routing tables and longest-prefix-match lookups in routers and firewalls.
- Radix trees: filesystem path tables, URL routers, and any large set of strings with long shared prefixes.
- Binary tries: maximum XOR pair/subarray problems, and IP-mask style bit manipulation.
- Ternary search tries: autocomplete and spell-check dictionaries where memory per node matters more than raw speed.
- Persistent tries: version-controlled dictionaries, undo/redo over prefix-searchable data, and functional-language string sets.
Alternatives worth weighing before reaching for any trie variant: a plain hash set gives $O(L)$ exact-match lookups with excellent cache behavior when prefix queries are not needed at all. A Bloom filter trades exactness for a fixed, tiny memory footprint when false positives are tolerable. Suffix trees and suffix arrays solve a different problem — substring search anywhere inside a text — rather than prefix search over a set of whole keys, and are worth reaching for only when that is the actual requirement.
9. Exercises
Easy
- By hand, insert
"go","gone","good"into an empty radix tree in that order, and draw the resulting node/edge structure at each step. - Given a working
insertfor a radix tree, implementsearchand verify it against the "test"/"team"/"toast" example above.
Medium
- Solve Maximum XOR of Two Numbers in an Array (LeetCode 421) using the binary trie above, then extend it to a variant: given a target value $v$ and a range, find the maximum XOR achievable with a value from the array that also falls within that range.
- Extend the binary trie query to return the pair of numbers achieving the maximum XOR, not just the XOR value itself.
Hard
- Implement autocomplete (prefix listing, ordered lexicographically) on top of a ternary search trie, using
keys_with_prefixas the foundation, and add a result-count limit for large dictionaries. - Add a
deleteoperation to the radix tree that merges a node back into its parent's edge when a deletion leaves it with a single child and no stored word.
Review rhythm: tomorrow, redraw the radix-tree split from the hand trace without looking; next week, re-implement the binary XOR trie and the ternary search trie from memory in one sitting; several weeks later, identify which of these four variants best fits a new prefix- or bit-oriented problem before writing any code.
10. Summary Cheat Sheet
- A compressed trie (radix tree) collapses single-child chains into substring-labeled edges, giving $O(k)$ nodes for $k$ keys.
- A Patricia trie is the bit-level version of a radix tree, storing a skip count per node instead of a substring — the structure behind router longest-prefix-match.
- A binary trie stores fixed-width bit representations; greedily preferring the opposite bit at each level solves maximum-XOR problems.
- A ternary search trie uses three children per node (less/equal/greater) instead of an array or hashmap, combining BST-like space efficiency with trie-like prefix search.
- A persistent trie uses path copying to keep every past version queryable, cloning only the $O(L)$ nodes on the modified path.
- Choose by workload: radix/Patricia for space and longest-prefix-match, binary trie for numeric XOR queries, TST for memory-constrained prefix search, and a plain hash set when prefix queries are not needed at all.