Last 30 Days
No notifications
A Trie (pronounced "try") is a tree-like data structure used for efficient string searching and prefix matching.
| Operation | Time | Description |
| Insert word | O(m) | m = word length |
| Search word | O(m) | Exact match |
| Prefix search | O(m) | All words with prefix |
| Delete word | O(m) | Mark as non-end |
A trie (pronounced "try", from retrieval) is the data structure for prefix-based string queries — autocomplete in the search bar, the spell-check underline in your editor, the routing table inside every internet router, the dictionary check inside word games. It's a hash table's underwhelming cousin for exact lookup, but its superpower is "give me everything that starts with…", which a hash table simply can't do efficiently.
A trie is a tree where each edge represents a single character and the path from the root to a node spells out a string. A boolean flag at each node says "a word ends here". The root is the empty string.
`
(root)
/ | \
a b c
/ \
p n a a
/ \
p* t*d*t r
l s* s*
|
e*
* = isEndOfWord
Words inserted: app, apt, and, apple, bats, cars
`
Every node stores:
isEndOfWord\ is independent of "has children". "app" can be a word AND have descendants like "apple" — the node for "app" has both \isEndOfWord = true\ AND a child 'l'.Picture a paper dictionary with thumb-tabs: A, B, C, …. Inside the A section there are sub-tabs Aa, Ab, …, Ap. Inside Ap you have App, Apt, … At each level you skip to the right tab in O(1) and the depth equals the length of the prefix you've typed. That's exactly what a trie does — except the "tabs" are pointers in a hash map.
1. Conflating "has children" with "isEndOfWord". "app" being a word doesn't preclude "apple". Always store the flag separately.
2. Allocating a 26-slot array for Unicode. That works for the LeetCode "lowercase English only" problems and breaks the moment a user types "café" or an emoji. For real apps, use a hash map.
3. Forgetting to clean up empty branches on delete. If you only flip the flag back to false, the trie grows forever. Recursive bottom-up cleanup: remove a child if it has no \isEndOfWord\ and no descendants.
4. Confusing trie with suffix tree. A trie of *words* answers "does this prefix exist?". A suffix tree of *one string* answers "does this substring exist?". Different problem, different structure.
5. Reaching for a trie when a HashSet would do. If you only need exact-match dictionary lookup, a hash set is faster and uses less memory. The trie is for *prefix operations*.
6. Storing the full word in every node. Wastes memory. Either reconstruct it from the path during DFS, or store it only at end-of-word nodes if you need it for collection.
| Feature | Trie | Hash Table |
| Exact lookup | O(m) | O(m) average, O(n) worst |
| Prefix search ("startsWith") | O(m) ✅ | O(n · m) — must scan every key |
| Autocomplete (all words with prefix) | natural — DFS from prefix node | not designed for it |
| Sorted iteration | free (DFS yields lexicographic order) | requires separate sort |
| Worst-case bound | O(m) guaranteed | O(n) under adversarial inputs |
| Space | many pointers per node | compact |
The key sentence: trie performance depends on word length, not dictionary size. With 100k words averaging 8 chars, lookup is O(8). The hash table is also O(8) — but the *prefix query* on the hash table is O(800k).
All four operations are O(m) where m is the word length. Independent of n (the number of stored words).
`js
class Trie {
constructor() { this.root = { children: {}, end: false }; }
insert(word) { let node = this.root; for (const ch of word) { if (!node.children[ch]) node.children[ch] = { children: {}, end: false }; node = node.children[ch]; } node.end = true; }
search(word) { const node = this.#walk(word); return !!node && node.end; }
startsWith(prefix) { return !!this.#walk(prefix); }
#walk(s) {
let node = this.root;
for (const ch of s) {
if (!node.children[ch]) return null;
node = node.children[ch];
}
return node;
}
}
`
Delete is the only one that takes thought: walk down to the end node, set \end = false\, then on the way back up, drop a child whenever it has no end flag and no children of its own.
| Array of 26 (or 256) | Hash map | |
| Lookup | O(1), cache-friendly | O(1) average, hash overhead |
| Memory | 26 pointers per node always | only allocated children |
| Alphabet | small, fixed (a-z) | any (Unicode, mixed case) |
| Best for | dense LeetCode tries | real-world text |
The array version is dramatically faster for tight loops over English (e.g., Word Search II on LeetCode) because of cache locality. The hash map version wins everywhere else.
A standard trie wastes nodes on long single-child chains — e.g. inserting only "banana" creates 6 nodes in a row, each with one child. A radix trie collapses any chain of single-child nodes into one edge labelled with the whole substring.
`
Plain trie: Radix trie:
t t
e est
| / \
s ing *
t *
`
Used in:
For numeric problems involving XOR, treat each integer as its 32-bit binary string and insert it into a binary trie (each node has children 0 and 1).
To find the maximum \a XOR b\ for some \a\ against everything in the trie:
`
for each bit of a, from MSB down:
prefer the OPPOSITE bit child if it exists (1 XOR 0 = 1, that bit contributes 2^k)
otherwise take the same-bit child
`
Insert n numbers in O(32 n) and answer each "max XOR with set" query in O(32). This collapses an O(n²) brute-force into O(n).
If you insert every suffix of one string into a trie, you can answer "is P a substring of S?" in O(
| P |
grep -P\..\ wildcard — DFS that branches into all children when the current char is \.\.dp[i]\ = "can s[0..i) be segmented?", and use the trie to enumerate matching prefixes from each position.1. Implement \Trie\ with \insert\ / \search\ / \startsWith\. Verify "app" can coexist with "apple" — both \search("app")\ and \search("apple")\ return true.
2. Add an \autocomplete(prefix, k)\ method that returns up to k words starting with \prefix\. Use DFS from the prefix node.
3. Solve Word Search II: build a trie from the word list, then DFS the 2D board pruning whenever the current path leaves the trie.
4. Solve Maximum XOR of Two Numbers in an Array using a bitwise trie. Confirm the O(32 n) bound by comparing against the O(n²) brute force on a 10k-element input.