Trie
Store a set of strings as a tree of characters so every shared prefix is stored once, making prefix lookups, autocomplete and multi-word searches cost O(length of the word) instead of O(number of words).
Instinct Many words, shared prefixes, and questions like "starts with", autocomplete, wildcard match or "search a board for all these words" - build a prefix tree.
Memory hack Words share their beginnings, so store each beginning once - walk one letter per step, and an end flag marks where a real word stops.
At a glance
When to use it
- Many words share beginnings and you query by prefix, such as startsWith or autocomplete
- Search patterns contain wildcards that match any letter
- You must find many dictionary words inside a grid or text in one pass
- You want the maximum XOR pair, using a trie over bits
Key idea
Store each shared prefix once as a path of characters, so insert, search and prefix queries cost O(L) for a word of length L no matter how many words are stored, and one walk prunes every word that cannot match instead of testing each word separately.
Typical complexity
O(L) time per operation, O(total characters) space
| # | Common problems | Level | Our solution | Practice |
|---|---|---|---|---|
| 1 | Implement Trie (Prefix Tree) | Medium | Read solution | LeetCode ↗ |
| 2 | Longest Word in Dictionary | Medium | Read solution | LeetCode ↗ |
| 3 | Design Add and Search Words Data Structure | Medium | Read solution | LeetCode ↗ |
| 4 | Search Suggestions System | Medium | Read solution | LeetCode ↗ |
| 5 | Maximum XOR of Two Numbers in an Array | Medium | Read solution | LeetCode ↗ |
| 6 | Word Search II | Hard | Read solution | LeetCode ↗ |
The instinct
A hash set answers "is this exact word present?" but is blind to prefixes: to find every word starting with "app" you would scan the whole set. A trie flips the storage around - each edge is one character, each node is a prefix, and words that begin the same way literally share a path. Asking about a prefix becomes walking down that path, which costs only the prefix length, no matter how many words are stored.
The same idea works on bits: a binary trie stores numbers bit by bit from the most significant end, which turns "maximum XOR pair" into a greedy walk.
Recognise it
| Signal in the problem | Why it points here |
|---|---|
| "Implement insert / search / startsWith" | The textbook trie API |
| "Autocomplete", "suggest up to 3 products after each typed character" | Each typed character moves one node deeper; suggestions live in that subtree |
". matches any letter" / wildcard search |
At a wildcard, branch into all children with DFS |
| "Find all dictionary words in a grid / text" | One trie walk prunes every word that cannot match the current path at once |
| "Longest word buildable one letter at a time", "shortest root to replace words" | Walk prefixes and check end flags along the way |
| "Maximum XOR of two numbers" | Binary trie over bits, greedily choose the opposite bit |
| Many queries against one fixed dictionary of short words | Pay build cost once; each query is O(word length) |
Not this pattern if...
- You only need exact-match lookups - a
HashSet<String>is simpler. - You must match one pattern inside a long text - KMP or rolling hash fits better.
- The "prefix" is numeric (prefix sums over an array) - unrelated despite the name.
Memory hack
"Store each beginning once." One letter per step down, one boolean (isEnd) to say "a real word stops here". Prefix query = can I finish the walk? Word query = can I finish the walk and is the end flag set?
Analogy - a phone's contact search. Type M and the list shrinks to names starting with M; type Ma and it shrinks again. The phone never rescans the whole contact list - it simply follows the branch you are typing. Every name that starts with "Ma" lives under the same branch.
Say it out loud in the interview:
- "Lots of words with shared prefixes - I'll build a trie: node =
children[26]+isEnd(or a stored word)." - "Insert and lookup walk one character per step - O(L) each."
- "For wildcards or grids, I DFS through the trie and prune the moment a child is missing."
How it works
Insert cat, car, cart, dog. The prefix ca is stored once and branches into t and r.
flowchart TD
R(("root")) --> C["c"]
R --> D["d"]
C --> A["a"]
A --> T["t (end: cat)"]
A --> R2["r (end: car)"]
R2 --> T2["t (end: cart)"]
D --> O["o"]
O --> G["g (end: dog)"]
search("car")walks c-a-r and finds the end flag: true.search("ca")walks c-a but the flag is off: false;startsWith("ca")is true.- In Word Search II, the grid DFS carries a trie node instead of a string. Moving onto a letter with no matching child stops that branch immediately, so all dictionary words are searched in a single sweep of the board.
Java template
import java.util.*; class Trie { private static class Node { Node[] next = new Node[26]; // swap for HashMap<Character, Node> on large alphabets boolean isEnd; // or: String word; int count; int prefixCount; } private final Node root = new Node(); void insert(String word) { Node cur = root; for (char ch : word.toCharArray()) { int i = ch - 'a'; if (cur.next[i] == null) cur.next[i] = new Node(); cur = cur.next[i]; } cur.isEnd = true; } boolean search(String word) { Node n = walk(word); return n != null && n.isEnd; } boolean startsWith(String prefix) { return walk(prefix) != null; } private Node walk(String s) { Node cur = root; for (char ch : s.toCharArray()) { cur = cur.next[ch - 'a']; if (cur == null) return null; } return cur; } // Wildcard search: '.' matches any single letter. boolean match(String pattern) { return dfs(root, pattern, 0); } private boolean dfs(Node node, String p, int i) { if (i == p.length()) return node.isEnd; char ch = p.charAt(i); if (ch != '.') { Node child = node.next[ch - 'a']; return child != null && dfs(child, p, i + 1); } for (Node child : node.next) { if (child != null && dfs(child, p, i + 1)) return true; } return false; } } // Binary trie: maximum XOR of any two numbers (non-negative ints). class BitTrie { private final int[][] next; private int count = 1; // node 0 is the root BitTrie(int n) { next = new int[31 * n + 1][2]; } // 31 bits per number at most void insert(int x) { int cur = 0; for (int b = 30; b >= 0; b--) { int bit = (x >> b) & 1; if (next[cur][bit] == 0) next[cur][bit] = count++; cur = next[cur][bit]; } } int bestXor(int x) { // call after at least one insert int cur = 0, res = 0; for (int b = 30; b >= 0; b--) { int bit = (x >> b) & 1, want = bit ^ 1; if (next[cur][want] != 0) { res |= 1 << b; cur = next[cur][want]; } else cur = next[cur][bit]; } return res; } }
Complexity
| Operation | Time | Space |
|---|---|---|
| Insert / search / startsWith | O(L) for a word of length L | O(total characters x alphabet) for array children |
| Wildcard search | O(L) best, up to O(26^dots x L) with many . |
O(L) recursion |
| Word Search II | roughly O(cells x 4 x 3^(L-1)) worst case, heavily pruned in practice | O(total dictionary characters) |
| Binary trie (max XOR) | O(n x bits) | O(n x bits) |
Array children are fast but memory hungry (26 references per node); a HashMap per node saves memory for sparse or Unicode alphabets at a constant-factor speed cost.
Variations
- Classic dictionary - insert/search/prefix. Implement Trie (Prefix Tree).
- Wildcard DFS - branch on
.. Design Add and Search Words Data Structure. - Grid + trie backtracking - store the full word at its end node, clear it once found, prune empty branches. Word Search II.
- Autocomplete / top suggestions - walk the typed prefix, then gather up to k words in lexicographic order (or keep a small sorted list at each node). Search Suggestions System.
- Prefix chain - every prefix must itself be a word. Longest Word in Dictionary (BFS/DFS only through end-flagged nodes).
- Bitwise trie - greedy opposite-bit walk. Maximum XOR of Two Numbers in an Array.
- Counting trie - store
prefixCountat each node for "how many words start with...".
Common mistakes
Confusing "path exists" with "word exists". After inserting "apple", search("app") must be false - always check the end flag, not just that the walk succeeded.
- Allocating a new root (or a new
Node) inside every call, so inserts never accumulate. - Hard-coding
ch - 'a'when input can contain uppercase letters, digits or other symbols. - In Word Search II, not marking the board cell as visited during DFS, or reporting the same word twice - null out the stored word after adding it.
- Rebuilding strings with
+at every DFS step instead of storing the word at the terminal node (quadratic garbage). - For the bit trie, iterating from the least significant bit - the greedy choice only works from the highest bit downward.
All practice problems
6 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.