Tries — Prefix Trees Built for Strings¶
A trie (pronounced “try,” short for retrieval) is a tree where edges represent characters and paths from root to a marked node represent complete words. It is not a hash map of strings — it’s a tree of characters that physically encodes shared prefixes. That structural difference is what gives it capabilities no flat hash map can match.
The defining use case: given a dictionary of 100,000 words, answer “does any word start with ‘pre’?” A hash map requires scanning every key. A trie answers in O(3) — one step per character in the prefix. That O(L) complexity for any prefix-related query is the trie’s fundamental value.
Trie Structure¶
Words inserted: ["apple", "app", "ape"]
root
|
a
|
p
/ \
p e
| |
l (end="ape")
|
e
|
(end="apple")
(also end="app" at second 'p')
Each node stores:
An array (or map) of child nodes — one slot per possible character
A boolean
isEnd— marks whether a complete word terminates at this node
class TrieNode {
TrieNode[] children;
boolean isEnd;
TrieNode() {
children = new TrieNode[26]; // for lowercase a-z
isEnd = false;
}
}
class Trie {
private final TrieNode root = new TrieNode();
// insert, search, startsWith below
}
Core Operations¶
Insert — O(L)¶
Walk the trie character by character. Create nodes where they don’t exist. Mark the final node as a word endpoint.
void insert(String word) {
TrieNode curr = root;
for (char c : word.toCharArray()) {
int idx = c - 'a';
if (curr.children[idx] == null) {
curr.children[idx] = new TrieNode();
}
curr = curr.children[idx];
}
curr.isEnd = true;
}
Search — O(L)¶
Walk the trie following the word’s characters. If any character has no corresponding child, the word is not in the trie. At the end of the word, check isEnd — this distinguishes between “apple” (present) and “app” (prefix only, if “app” was never inserted separately).
boolean search(String word) {
TrieNode curr = root;
for (char c : word.toCharArray()) {
int idx = c - 'a';
if (curr.children[idx] == null) return false;
curr = curr.children[idx];
}
return curr.isEnd; // true only if a complete word ends here
}
startsWith — O(L)¶
Identical to search, except the final isEnd check is dropped. Any path that exists in the trie qualifies as a valid prefix.
boolean startsWith(String prefix) {
TrieNode curr = root;
for (char c : prefix.toCharArray()) {
int idx = c - 'a';
if (curr.children[idx] == null) return false;
curr = curr.children[idx];
}
return true; // the prefix path exists
}
What most people get wrong: confusing
searchandstartsWith. The only functional difference is the finalisEndcheck. If only “apple” is inserted:search("app")returns false;startsWith("app")returns true. Get this mixed up on LC 208 and you fail the easy problem.
Space Complexity: The Fan-Out Problem¶
Each TrieNode holds 26 child pointers for lowercase English. With n nodes in the trie, that’s 26n pointer slots — most of which are null for typical input. The constant factor of 26 is real in memory even if asymptotically it’s O(n).
For Unicode or large alphabets, use a HashMap<Character, TrieNode> per node:
class TrieNodeMap {
Map<Character, TrieNodeMap> children = new HashMap<>();
boolean isEnd = false;
}
Trade-off:
Implementation |
Space Used |
Access Time |
When to Use |
|---|---|---|---|
Array |
26 × nodes (sparse) |
O(1), direct index |
Known small alphabet (a-z) |
HashMap per node |
proportional to actual edges |
O(1) avg, hash overhead |
Large or unknown alphabet |
For LeetCode (typically lowercase a-z), the array implementation is cleaner and faster. For a real autocomplete system supporting Unicode, you’d use a hash map or compressed trie.
Compressed Tries / Patricia Tries (Intuition)¶
A standard trie has a problem with sparse paths: the word “elephant” stored alone creates a chain of 8 nodes for e-l-e-p-h-a-n-t that could be one edge labeled “elephant.”
A Patricia trie (Practical Algorithm To Retrieve Information Coded In Alphanumeric) compresses non-branching chains into single labeled edges. A branch occurs only where two stored words diverge in their characters.
You will not implement a Patricia trie for LeetCode. Understanding that they exist explains why production systems (autocomplete backends, routing tables, DNS resolution) don’t use a naive trie with one character per node. Most use compressed variants or radix trees to control memory.
Applications¶
Autocomplete: traverse to the prefix node, then DFS to collect all
isEndnodes below — every path is a word completion suggestionSpell check: search for a word; if not found, enumerate nearby words by single-character substitution or deletion
IP routing (longest prefix match): store network prefixes in a binary trie (bits instead of characters); longest matching prefix determines the routing decision
Word Search II (LC 212): combine a trie with backtracking on a grid — simultaneously match multiple dictionary words instead of searching separately for each
Word Search II: Trie + Backtracking Combined¶
This is the payoff problem that justifies learning tries in the context of Phase 2. LC 212 asks for all dictionary words present in a character grid.
Naive approach: for each word in the dictionary, run backtracking on the grid independently. Cost: O(W × 4^(MN)) where W is number of words.
Trie approach: insert all words into a trie. During grid backtracking, walk the trie in sync with the grid path. If the current cell’s character has no child in the current trie node, prune immediately — no word with this prefix exists.
// Key logic inside the backtracking function:
void dfs(char[][] board, int i, int j, TrieNode node) {
char c = board[i][j];
int idx = c - 'a';
if (node.children[idx] == null) return; // PRUNING: no word has this prefix
TrieNode next = node.children[idx];
if (next.isEnd) {
result.add(next.word); // found a word
next.isEnd = false; // avoid adding duplicates
next.word = null;
}
board[i][j] = '#'; // mark visited
// recurse on 4 neighbors
board[i][j] = c; // restore
// Optimization: prune dead trie branches (no children left, not a word)
if (isEmpty(next)) node.children[idx] = null;
}
Storing the complete word in each isEnd node (instead of reconstructing from path) is a practical optimization that avoids rebuilding the string from the grid path.
Practice Problems¶
# |
Problem |
Difficulty |
Focus |
|---|---|---|---|
1 |
LC 208 — Implement Trie |
Easy |
Core insert/search/startsWith |
2 |
LC 648 — Replace Words |
Medium |
Trie for shortest prefix lookup |
3 |
LC 211 — Design Add and Search Words Data Structure |
Medium |
Wildcard ‘.’ — DFS through all children |
4 |
LC 212 — Word Search II |
Hard |
Trie + backtracking, branch pruning |
Start with LC 208. Implement from scratch in 20 minutes without reference. If you can do that, you understand the structure. LC 211 adds a twist: the ‘.’ wildcard in search must try all non-null children — it’s a DFS on the trie itself. LC 212 is the boss fight for Phase 2.