Tries (Prefix Trees)
โฑ๏ธ ~3-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐๏ธ A branching filing cabinet where every shared prefix shares the same drawer.
The core idea
Store the words ["car", "cart", "cat", "bat", "ball"]in a trie. "cat" and "car" share the "ca" drawer. You only need one "c" node, one "a" node โ then they branch. A node with a star (โ
) means a complete word ends there.
Trie for ["car","cart","cat","bat","ball"]
root
โโโ c
โ โโโ a
โ โโโ r โ (car)
โ โ โโโ t โ (cart)
โ โโโ t โ (cat)
โโโ b
โโโ a
โโโ t โ (bat)
โโโ l
โโโ l โ (ball)
Why search is O(L) regardless of dictionary size
To look up "cat" you traverse exactly 3 nodes: c โ a โ t. It doesn't matter if the trie has 100 words or 10 million โ you only ever walk L steps where L is the length of the query word. A hash map gives O(1) average, but can't do prefix matching. A sorted array gives O(log n) per lookup, but prefix search requires extra work. A trie gives O(L) for both.
When to reach for a trie
- Problem involves prefix matching or autocomplete
- You need to find all words starting with a given prefix
- You need to check if any word in a set starts with a given string
- IP routing โ longest prefix match in routing tables
Interactive Sandbox
โ Move something, see it react instantly.Challenge
Explore all 5 trie operations: Insert, Search, Prefix, Delete, Autocomplete.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview question: Trie vs HashMap for autocomplete?
A HashMap<String, Boolean> can answer "does this exact word exist?" in O(1), but to find all words with prefix "ca" you must scan every key โ O(n) where n is dictionary size. A trie traverses only the relevant subtree, making prefix enumeration O(P + k) where P = prefix length and k = number of matches.
| 1 | class TrieNode { |
| 2 | children = new Map<string, TrieNode>(); |
| 3 | isEnd = false; |
| 4 | } |
| 5 | class Trie { |
| 6 | root = new TrieNode(); |
| 7 | ย |
| 8 | insert(word: string) { |
| 9 | let node = this.root; |
| 10 | for (const c of word) { |
| 11 | if (!node.children.has(c)) |
| 12 | node.children.set(c, new TrieNode()); |
| 13 | node = node.children.get(c)!; |
| 14 | } |
| 15 | node.isEnd = true; |
| 16 | } |
| 17 | ย |
| 18 | search(word: string): boolean { |
| 19 | const node = this.#traverse(word); |
| 20 | return node?.isEnd ?? false; // must check isEnd! |
| 21 | } |
| 22 | ย |
| 23 | startsWith(prefix: string): boolean { |
| 24 | return this.#traverse(prefix) !== null; |
| 25 | } |
| 26 | ย |
| 27 | #traverse(s: string): TrieNode | null { |
| 28 | let node: TrieNode = this.root; |
| 29 | for (const c of s) { |
| 30 | if (!node.children.has(c)) return null; |
| 31 | node = node.children.get(c)!; |
| 32 | } |
| 33 | return node; |
| 34 | } |
| 35 | } |
| 36 | // Time: O(L) per op ยท Space: O(alphabet ร total_chars) |
Interview question: Space complexity โ when is a trie wasteful?
Each node holds a Map (or array of 26) of children. Worst case: n words with no shared prefixes (e.g. random UUIDs). You get O(n ร L) nodes โ similar to storing the strings themselves but with per-node overhead. A trie shines when the vocabulary has lots of shared prefixes. Radix trees (compressed tries) merge single-child chains to cut space dramatically.
Interview question: Count words with a given prefix?
Store a count field on each node incremented during insert. A prefix query becomes a single traversal returning the count at the prefix node โ O(L). No DFS needed. This is a common follow-up after the basic trie implementation.
Classic bug: forgetting the isEnd check
If you return true whenever traverse(word)returns a non-null node, you get false positives. Searching for "ca" would return true even if only "car" and "cat" are in the trie, because the "a" node exists. Always check node.isEnd for exact search.
The Deep Dive
โ Spec refs, engine internals, the minutiae.Compressed trie (Radix tree / Patricia trie)
A plain trie wastes nodes on single-child chains. The word "ball" creates b โ a โ l โ l โ four nodes, three of which have only one child. A radix tree merges single-child chains into edge labels: b โ a โ llโ . Lookup is the same O(L) but with far fewer node allocations. This is how most production implementations work.
Radix tree for ["car","cart","cat"]
root
โโโ ca
โโโ r โ (car)
โ โโโ t โ (cart)
โโโ t โ (cat)
Aho-Corasick: multi-pattern string search
A trie matches one pattern at a time. Aho-Corasick adds "failure links" โ when a match fails at a node, jump to the longest proper suffix that is a valid prefix in the trie. This lets you search a text for all k patterns simultaneously in O(n + m + z) where n = text length, m = total pattern length, z = number of matches. Used in antivirus engines and network intrusion detection.
Suffix trie vs Trie
A trie indexes a set of words. A suffix trie indexes all suffixes of a single string โ enabling O(L) substring search in a long document. Suffix tries are huge (O(nยฒ) nodes for a string of length n), so in practice you use a suffix array + LCP array for the same queries with O(n log n) build time and O(n) space.
IP routing โ longest prefix match
IPv4 routing tables store CIDR prefixes (e.g. 192.168.1.0/24). To forward a packet, you need the most specific (longest) matching prefix โ not just any match. A binary trie over the 32-bit address gives O(32) = O(1) lookup with exact longest-prefix semantics. Hardware routers implement this in silicon as TCAM (ternary content-addressable memory), but the conceptual model is a binary trie.
Real-world appearances
- Google / browser autocomplete โ trie over typed prefix; top-k results ranked by weight
- Spell checkers โ traverse with bounded edit distance (BK-tree or trie + DFS with budget)
- DNS resolution โ hierarchical label lookup is conceptually a trie (. โ com โ example)
- Git object addressing โ object IDs indexed by hex prefix in .git/objects/
Interview Questions
โ Real questions from real interviews โ with answers.All three are O(m) where m is the length of the word โ independent of how many words are stored.
Traverse to the prefix node, then DFS/BFS to collect all words in that subtree.
Trie uses more memory but enables O(m) prefix queries; hashset is compact but can't answer 'does any word start with X?'
BFS/DFS with an edit-distance budget โ explore neighbors within bounded Levenshtein distance.
Radix tree collapses single-child chains into one edge โ reduces node count from O(total chars) to O(n * key branches).
Memory Game
โ Quick quiz โ lock the concept in long-term memory.What is a suffix trie used for, and why isn't it used in production?