Trie (Prefix Tree) — The Complete Guide
Master the Trie from first principles. Learn to recognize prefix-based lookup problems instantly, build the data structure from scratch, and confidently solve autocomplete, word search, and dictionary problems in interviews.
Table of Contents
Intuition from Scratch
Why does this pattern exist?
Imagine you're building a phone's autocomplete feature. The user types "app" and you need to instantly suggest "apple", "application", "append". You have a dictionary of 100,000 words. How do you find all words starting with "app" without scanning every single word?
A hash map can tell you if "apple" exists in O(1), but it can't efficiently find all words sharing a prefix. A sorted array with binary search can find the range, but inserting new words is O(n). What you need is a data structure built specifically for prefix-based operations — and that's a Trie.
A Trie (pronounced "try", from retrieval) is a tree where each node represents a single character. A path from root to a node spells out a prefix. A path from root to a node marked as "end of word" spells out a complete word. This structure lets you insert, search, and find all words with a given prefix in O(L) time, where L is the length of the word or prefix — completely independent of how many words are in the dictionary.
How a Trie Works (The Basics)
(root) / \ a b / \ p a / \ \ p e* t* | | l l* | e* * = end of word marker Paths from root: a → p → p → l → e = "apple" ✓ (e is marked) a → p → p = "app" ✓ (second p is marked) a → p → e = "ape" ✓ (e is marked) b → a → t = "bat" ✓ (t is marked) b → a → l → l = "ball" ✓ (second l is marked) Key insight: "apple" and "app" share the path a→p→p. The shared prefix is stored ONCE, not twice.
Analogies to Build Intuition
A Physical Dictionary
When you look up 'program' in a dictionary, you don't scan every word. You go to the 'P' section, then 'Pr', then 'Pro', narrowing down with each letter. A Trie works the same way — each level narrows the search by one character. By the time you've followed 7 edges, you've found your word among millions.
A File System
Your computer's folder structure is a Trie! /users/john/documents and /users/john/photos share the path /users/john/. Each folder is a node, each name segment is an edge. 'Find all files under /users/john/' is a prefix search — exactly what Tries excel at.
Phone Autocomplete
When you type 'hel', your phone suggests 'hello', 'help', 'helmet'. It's walking a Trie: follow h→e→l, then collect all words reachable from that node. The Trie doesn't scan all 100,000 words — it jumps directly to the 'hel' subtree.
What kind of problems does it solve?
Tries are your go-to when:
- You need prefix-based lookups — find all words starting with a prefix
- You need to search for words in a grid (Word Search II) — Trie prunes dead-end paths
- You need autocomplete or typeahead suggestions
- You need to check if any prefix of a word exists in a dictionary
- You need to find the longest common prefix among strings
- You need XOR-based optimization — binary Tries for maximum XOR pair
The core insight
A Trie trades space for time by sharing prefixes. Words with common beginnings share the same path in the tree. This makes prefix operations O(L) regardless of dictionary size — you only traverse as many nodes as there are characters in your query. Hash maps can't do this; sorted arrays are slower for dynamic data.
Pattern Recognition
Recognizing when a Trie is the right tool is the most important skill. Here are the signals to look for and the traps to avoid.
Keywords & Signals in the Problem Statement
Use Trie when you see...
- ✅"Implement autocomplete / typeahead"
- ✅"Find all words with prefix X"
- ✅"Search for words in a 2D grid" (Word Search II)
- ✅"Check if any prefix of word exists in dictionary"
- ✅"Longest word that can be built one character at a time"
- ✅"Replace words with their shortest root in a dictionary"
- ✅"Design a search engine with wildcard support (.)"
- ✅"Maximum XOR of two numbers" (binary Trie)
- ✅Multiple string lookups against the same dictionary
- ✅"Palindrome pairs" — need efficient prefix/suffix matching
Don't use Trie when...
- ❌You only need exact word lookup — a HashSet is simpler and faster
- ❌You need substring matching (not prefix) — use KMP, Rabin-Karp, or suffix structures
- ❌The alphabet is huge (e.g., Unicode) — Trie nodes become memory-heavy
- ❌You only have a few words — the overhead isn't worth it
- ❌You need sorted iteration — a balanced BST or sorted array is better
- ❌The problem is about contiguous subarrays — use Sliding Window or Prefix Sum
Trie vs. Similar Approaches
| Approach | When to Use | Key Difference |
|---|---|---|
| Trie | Prefix search, autocomplete, word search in grid, multiple prefix queries | O(L) per operation. Shares prefixes. Best when many queries hit the same dictionary. |
| HashSet / HashMap | Exact word lookup, frequency counting | O(L) per lookup (hashing the string). Can't do prefix search efficiently. |
| Sorted Array + Binary Search | Prefix range queries on static data | O(L log n) for prefix search. No dynamic inserts. Simpler but slower for many queries. |
| Suffix Tree / Array | Substring matching, longest repeated substring | Handles substrings, not just prefixes. More complex to build. Overkill for prefix problems. |
| Aho-Corasick | Multi-pattern string matching in a text | Built on a Trie with failure links. For searching multiple patterns in one pass through text. |
The Trie litmus test
Ask yourself: "Does this problem involve prefix-based operations on a collection of strings?" If yes, Trie. If you only need exact lookups, a HashSet is simpler. If you need substring matching, look at suffix structures. The word "prefix" is your strongest signal.
Brute Force → Optimized Thinking
Let's walk through the thinking evolution using a concrete example: Autocomplete — given a dictionary of words and a prefix, return all words starting with that prefix.
Brute Force — Scan Every Word
Store all words in an array. For each query, iterate through every word and check if it starts with the prefix. This works but is painfully slow when you have millions of words and thousands of queries.
function autocomplete(words: string[], prefix: string): string[] { return words.filter(word => word.startsWith(prefix)); } // For each query: O(n) words × O(L) per startsWith check // 1 million words × 1000 queries = 1 billion operations. Too slow.
Better — Sort + Binary Search
Sort the words lexicographically. Use binary search to find the first and last word with the given prefix. All words in that range match. Better, but binary search on strings is O(L log n) per query, and inserting new words requires re-sorting.
// After sorting: ["ape", "app", "apple", "bat", "ball"] // Prefix "app" → binary search finds range [1, 2] → ["app", "apple"] // O(L log n) per query — better than O(nL), but: // - Inserting a new word is O(n) (shift elements) // - Each binary search comparison is O(L) (string comparison)
Optimal — Trie
Build a Trie from all words. For a prefix query, walk down the Trie following the prefix characters (O(P) where P = prefix length), then collect all words in that subtree (O(K) where K = number of matching words). Insertions are also O(L). This is optimal for dynamic dictionaries with many queries.
// Build Trie: insert each word → O(n × L) total // Query "app": // 1. Walk root → a → p → p (3 steps = prefix length) // 2. DFS from that node to collect all words below // Result: ["app", "apple"] // // Time per query: O(P + K) where P = prefix length, K = results // This is INDEPENDENT of dictionary size n. // 1 million words, prefix "app" → still just 3 steps + results.
Why Does the Trie Optimization Work?
Shared prefixes eliminate redundant comparisons
In brute force, checking 'apple' and 'application' against prefix 'app' means comparing 'a', 'p', 'p' twice. In a Trie, both words share the same a→p→p path. You walk it once and reach both words.
Navigation is O(L), not O(n)
To find the prefix node, you follow exactly L edges (one per character). You never look at words that don't share the prefix. With 1 million words and prefix 'app', you take 3 steps — not 1 million.
Dynamic insertions are cheap
Adding a new word is O(L) — just walk down, creating nodes as needed. No re-sorting, no rehashing. This makes Tries ideal for streaming/dynamic dictionaries.
The thinking process matters more than the code
In interviews, walk through this progression: "Brute force scans all n words per query — O(nL). Sorting gives O(L log n) per query but O(n) inserts. A Trie gives O(P) per prefix lookup and O(L) inserts, making it optimal for a dynamic dictionary with many prefix queries."
Core Templates
Trie problems follow a few recurring templates. Memorize the node structure and these operations — most problems are variations of them.
The Trie Node & Basic Operations
class TrieNode { children: Map<string, TrieNode> = new Map(); isEnd: boolean = false; // Optional: store the word itself, count, or other metadata word?: string; } class Trie { root = new TrieNode(); /** Insert a word into the Trie — O(L) */ insert(word: string): void { let node = this.root; for (const ch of word) { if (!node.children.has(ch)) { node.children.set(ch, new TrieNode()); } node = node.children.get(ch)!; } node.isEnd = true; node.word = word; // useful for Word Search II } /** Check if a word exists exactly — O(L) */ search(word: string): boolean { const node = this._walkTo(word); return node !== null && node.isEnd; } /** Check if any word starts with this prefix — O(P) */ startsWith(prefix: string): boolean { return this._walkTo(prefix) !== null; } /** Walk to the node at the end of the given string */ private _walkTo(s: string): TrieNode | null { let node = this.root; for (const ch of s) { if (!node.children.has(ch)) return null; node = node.children.get(ch)!; } return node; } } // Usage: // const trie = new Trie(); // trie.insert("apple"); // trie.search("apple"); → true // trie.search("app"); → false (not marked as end) // trie.startsWith("app"); → true (prefix exists)
Template 1: Collect All Words with Prefix
Walk to the prefix node, then DFS to collect all words in that subtree. This is the autocomplete pattern.
function autocomplete(trie: Trie, prefix: string): string[] { const results: string[] = []; const prefixNode = trie._walkTo(prefix); if (!prefixNode) return results; // DFS from prefix node to collect all words function dfs(node: TrieNode, path: string): void { if (node.isEnd) results.push(path); for (const [ch, child] of node.children) { dfs(child, path + ch); } } dfs(prefixNode, prefix); return results; } // Time: O(P + K × avgLen) where P = prefix length, K = matching words // This is the foundation for autocomplete, search suggestions, etc.
Template 2: Word Search II (Trie + Backtracking)
Build a Trie from the word list. Then backtrack on the grid, walking the Trie simultaneously. The Trie prunes paths that can't form any word — far more efficient than checking each word independently.
function findWords(board: string[][], words: string[]): string[] { const trie = new Trie(); for (const word of words) trie.insert(word); const result: string[] = []; const rows = board.length, cols = board[0].length; function backtrack(r: number, c: number, node: TrieNode): void { // Prune: out of bounds, visited, or no Trie path if (r < 0 || r >= rows || c < 0 || c >= cols) return; const ch = board[r][c]; if (ch === '#' || !node.children.has(ch)) return; const nextNode = node.children.get(ch)!; // Found a word! if (nextNode.isEnd) { result.push(nextNode.word!); nextNode.isEnd = false; // avoid duplicates } // Choose: mark visited board[r][c] = '#'; // Explore: 4 directions backtrack(r + 1, c, nextNode); backtrack(r - 1, c, nextNode); backtrack(r, c + 1, nextNode); backtrack(r, c - 1, nextNode); // Unchoose: restore board[r][c] = ch; // Optimization: prune empty Trie branches if (nextNode.children.size === 0) { node.children.delete(ch); } } for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { backtrack(r, c, trie.root); } } return result; } // Key: the Trie guides the backtracking — you only explore paths // that could form a word. Without the Trie, you'd run a separate // backtracking search for each word (much slower).
Template 3: Wildcard Search (. matches any character)
When the search pattern contains wildcards, you can't just walk a single path. At each '.', branch into all children. This is DFS on the Trie.
class WordDictionary { root = new TrieNode(); addWord(word: string): void { let node = this.root; for (const ch of word) { if (!node.children.has(ch)) { node.children.set(ch, new TrieNode()); } node = node.children.get(ch)!; } node.isEnd = true; } search(word: string): boolean { return this._dfs(word, 0, this.root); } private _dfs(word: string, idx: number, node: TrieNode): boolean { if (idx === word.length) return node.isEnd; const ch = word[idx]; if (ch === '.') { // Wildcard: try ALL children for (const child of node.children.values()) { if (this._dfs(word, idx + 1, child)) return true; } return false; } else { // Exact match: follow the specific child if (!node.children.has(ch)) return false; return this._dfs(word, idx + 1, node.children.get(ch)!); } } } // Time: O(L) for exact search, O(26^dots × L) worst case for wildcards // Key: '.' branches into all 26 children — exponential in # of dots // but practical because most paths die quickly
Which template to pick?
Ask yourself: (1) Do I need prefix lookup or autocomplete? → Basic Trie + DFS collection. (2) Am I searching for words in a grid? → Template 2 (Trie + Backtracking). (3) Does the search support wildcards? → Template 3 (DFS with branching on dots). (4) Am I checking if any prefix of a word is in the dictionary? → Walk the Trie, check isEnd at each step.
Variations & Sub-Patterns
The Trie isn't a single trick — it's a versatile structure with several important variations.
| Variation | Key Modification | Classic Example |
|---|---|---|
| Basic Trie (insert/search/prefix) | Standard implementation with isEnd flag | Implement Trie, Search Suggestions System |
| Trie + Backtracking (grid) | Walk Trie and grid simultaneously; Trie prunes invalid paths | Word Search II |
| Wildcard Trie | On '.', branch into all children via DFS | Design Add and Search Words Data Structure |
| Counting Trie | Store count at each node (prefix count) and at end (word count) | Count Prefixes of a Given String, Map Sum Pairs |
| Trie with deletion | Decrement counts on delete; remove nodes when count reaches 0 | Implement Trie with delete operation |
| Binary Trie (XOR) | Each node has children 0 and 1; insert numbers bit by bit | Maximum XOR of Two Numbers in an Array |
| Trie for word replacement | Walk each word through Trie; if isEnd is hit before word ends, replace with the prefix | Replace Words, Longest Word in Dictionary |
Problems mentioned above
Deep Dive: Binary Trie for Maximum XOR
This is the most surprising Trie application. To find the maximum XOR of any two numbers in an array, insert each number into a binary Trie (bit by bit, from MSB to LSB). For each number, greedily walk the Trie choosing the opposite bit at each level to maximize XOR.
function findMaximumXOR(nums: number[]): number { // Binary Trie node: children[0] and children[1] const root: [any, any] = [null, null]; function insert(num: number): void { let node = root; for (let i = 31; i >= 0; i--) { const bit = (num >> i) & 1; if (!node[bit]) node[bit] = [null, null]; node = node[bit]; } } function maxXorWith(num: number): number { let node = root; let xor = 0; for (let i = 31; i >= 0; i--) { const bit = (num >> i) & 1; const opposite = 1 - bit; // Greedily pick the opposite bit if it exists if (node[opposite]) { xor |= (1 << i); node = node[opposite]; } else { node = node[bit]; } } return xor; } let maxResult = 0; for (const num of nums) { insert(num); maxResult = Math.max(maxResult, maxXorWith(num)); } return maxResult; } // Time: O(n × 32) = O(n) Space: O(n × 32) // Key insight: XOR is maximized when bits differ. At each level, // greedily choose the opposite bit. The Trie lets you do this // in O(32) per number instead of O(n) brute-force comparisons.
Deep Dive: Replace Words (Shortest Root)
Given a dictionary of roots and a sentence, replace each word with its shortest root. Walk each word through the Trie — the moment you hit an isEnd node, that's the shortest root.
function replaceWords(dictionary: string[], sentence: string): string { const trie = new Trie(); for (const root of dictionary) trie.insert(root); return sentence.split(' ').map(word => { let node = trie.root; for (let i = 0; i < word.length; i++) { const ch = word[i]; if (!node.children.has(ch)) break; // no matching root node = node.children.get(ch)!; if (node.isEnd) return word.slice(0, i + 1); // shortest root found! } return word; // no root found, keep original }).join(' '); } // Time: O(D × L + S × W) where D = dict size, S = sentence words // Key: stop at the FIRST isEnd — that's the shortest root. // The Trie naturally gives you the shortest prefix match.
Visual Walkthrough
Let's trace through two key Trie operations step by step to build deep intuition.
Building a Trie — Insert "apple", "app", "ape", "bat"
Start: root = {} Insert "apple": root → a (new) → p (new) → p (new) → l (new) → e (new, isEnd=true) Tree: (root) | a | p | p | l | e* Insert "app": root → a (exists) → p (exists) → p (exists, mark isEnd=true) Tree: (root) No new nodes created! | "app" shares the path with "apple" a | p | p* ← now marked as end | l | e* Insert "ape": root → a (exists) → p (exists) → e (new, isEnd=true) Tree: (root) | a | p / \ p* e* ← "ape" branches off at the second 'p' level | l | e* Insert "bat": root → b (new) → a (new) → t (new, isEnd=true) Tree: (root) / \ a b | | p a / \ | p* e* t* | l | e* Final Trie stores 4 words using 9 nodes (not 4×3.5 = 14 characters). Shared prefix "ap" saves 2 nodes between "apple"/"app" and "ape".
Searching with Wildcards — Pattern "a.e"
Trie contains: "apple", "app", "ape", "bat" Search pattern: "a.e" (. matches any single character) Step 1: idx=0, char='a' Exact match → follow 'a' child Current node: 'a' Step 2: idx=1, char='.' Wildcard! Branch into ALL children of 'a': Branch A: follow 'p' child Step 3a: idx=2, char='e' Check 'p' node's children for 'e': 'p' has children: {p, e} Branch A1: follow 'p' → idx=3, but word is only 3 chars idx === word.length → check isEnd on 'p' node 'p' node isEnd = true (word "app") But wait — we're at idx=3 and word length is 3, so we check the node we landed on. We followed a→p→p, isEnd=true. But the pattern is "a.e" — we need 'e' at position 2, not 'p'. Actually let me re-trace... Let me redo this more carefully: Node at 'a' has children: {p} Branch: follow 'p' (the only child) Step 3: idx=2, char='e' Node at 'p' (second level) has children: {p, e} Check for 'e': YES, 'e' exists! Follow 'e' → idx=3 idx === word.length (3) → check isEnd 'e' node isEnd = true → MATCH FOUND! ✓ This matched the word "ape" (a → p → e) Result: "a.e" matches "ape" ✓ Key observations: - '.' at position 1 tried all children of 'a' (just 'p' here) - With a larger alphabet, '.' would branch into up to 26 paths - Most branches die quickly — practical performance is much better than the theoretical O(26^dots) worst case
Practice Problems
These 7 problems are carefully ordered from easy to hard. Each one teaches a different aspect of the Trie pattern. Solve them in order.
LC 208. Implement Trie (Prefix Tree)
Why this pattern:
The foundational Trie problem. Implement insert, search, and startsWith from scratch. This is the building block for every other Trie problem — you must be able to write this in your sleep.
Key idea: TrieNode has a children map and an isEnd flag. Insert: walk down, create nodes as needed, mark end. Search: walk down, return isEnd. StartsWith: walk down, return true if you reach the end of the prefix without hitting null.
LC 14. Longest Common Prefix
Why this pattern:
Can be solved without a Trie (vertical scanning), but the Trie approach builds intuition. Insert all words, then walk from root following the only-child path until you hit a branch or an end marker.
Key idea: Insert all strings into a Trie. Walk from root: at each node, if there's exactly one child and it's not an end-of-word, continue. Stop when you branch or hit an end. The path so far is the longest common prefix.
LC 648. Replace Words
Why this pattern:
Trie for shortest prefix matching. Build a Trie from the dictionary roots. For each word in the sentence, walk the Trie — the first isEnd you hit is the shortest root replacement.
Key idea: Insert all roots into a Trie. For each word, walk character by character. If you hit isEnd, replace the word with the prefix up to that point. If you fall off the Trie, keep the original word.
LC 211. Design Add and Search Words Data Structure
Why this pattern:
Trie with wildcard support. addWord is standard insert. search uses DFS — on '.', branch into all children. On a regular character, follow the specific child.
Key idea: The key insight is that '.' forces you to try all 26 possible children at that position. Use recursive DFS: for '.', loop through all children and recurse. For a letter, follow that child. Return true if any path reaches isEnd at the end of the pattern.
LC 1268. Search Suggestions System
Why this pattern:
Trie for autocomplete. After each character typed, walk the Trie to that prefix node and collect the lexicographically smallest 3 words via DFS. Sorting the input first ensures DFS order is lexicographic.
Key idea: Sort products first. Insert into Trie. For each prefix (first char, first two chars, etc.), walk to the prefix node and DFS to collect up to 3 words. Since products were sorted, DFS naturally yields lexicographic order.
LC 212. Word Search II
Why this pattern:
Trie + Backtracking on a grid. Build a Trie from the word list. Backtrack on the grid, walking the Trie simultaneously. The Trie prunes paths that can't form any word — massively faster than running Word Search I for each word.
Key idea: Insert all words into a Trie (store the word at end nodes). For each cell, start backtracking with the Trie root. At each step, check if the current character has a Trie child. If a word is found, add it. Prune empty Trie branches for optimization.
LC 336. Palindrome Pairs
Why this pattern:
Advanced Trie application. Insert reversed words into a Trie. For each word, walk the Trie — if you reach an end node with remaining characters that form a palindrome, that's a pair. Also check if the Trie path extends beyond the word with a palindromic suffix.
Key idea: Insert each reversed word into a Trie, storing the word index. For word[i], walk the Trie with word[i]'s characters. At each isEnd node, check if the remaining suffix of word[i] is a palindrome. Also, at the end of word[i], check if any deeper Trie paths have palindromic remainders.
Practice strategy
For each problem: (1) Implement the basic Trie first (problem 1). (2) Identify what extra information each node needs (count? word? index?). (3) Determine the traversal pattern (walk to prefix? DFS? backtracking?). (4) After solving, note which template it used and what variation was needed.
Common Mistakes
These are the traps that catch people on Trie problems. Learn them here so you don't learn them in an interview.
Confusing 'prefix exists' with 'word exists'
You implement search() but forget to check isEnd. The Trie contains 'apple' — search('app') returns true because the path exists, but 'app' was never inserted as a complete word.
✅search() must check node.isEnd at the final node. startsWith() does NOT check isEnd — it only verifies the path exists. These are two different operations. Always be clear about which one you need.
Using an array[26] when a Map is cleaner
You use children = new Array(26).fill(null) for the children, then struggle with index math (ch.charCodeAt(0) - 97). This is error-prone and doesn't generalize to non-lowercase inputs.
✅Use a Map<string, TrieNode> for children. It's cleaner, handles any character set, and the performance difference is negligible in interviews. Only use array[26] if the problem explicitly constrains to lowercase letters and you need maximum speed.
Not storing the word at end nodes
In Word Search II, you reconstruct the word by tracking the path during backtracking. This is error-prone and adds complexity.
✅Store the complete word at the end node: node.word = word during insert. When you hit isEnd during backtracking, just grab node.word. Much simpler and less error-prone.
Not pruning the Trie in Word Search II
After finding a word in Word Search II, you leave the Trie intact. This means you might find the same word again from a different starting cell, producing duplicates.
✅After finding a word, set node.isEnd = false to prevent duplicates. Even better: prune empty branches by deleting children with no remaining words. This dramatically speeds up subsequent searches.
Excessive memory usage
Each Trie node with a Map or array[26] uses significant memory. For large dictionaries, the Trie can use more memory than a simple HashSet.
✅Be aware of the tradeoff: Tries trade space for prefix-query speed. If you only need exact lookups, a HashSet is more memory-efficient. In interviews, mention this tradeoff. For optimization, consider compressed Tries (radix trees) where single-child chains are collapsed.
Forgetting edge cases with empty strings
Inserting an empty string marks root.isEnd = true. Now startsWith('') returns true for everything, and search('') returns true. This may or may not be desired.
✅Clarify with the interviewer: can words be empty? If not, add a guard: if (word.length === 0) return. If yes, handle root.isEnd correctly in your logic.
Interview Strategy
Knowing the pattern is half the battle. Here's how to present it in an interview setting to maximize your score.
The 5-Step Interview Flow
Recognize the Prefix Signal
'This problem involves prefix-based lookups / autocomplete / searching multiple words in a grid. A Trie is the natural data structure because it gives O(L) prefix operations and shares common prefixes to save work.'
Explain Why Not a HashSet
'A HashSet gives O(L) exact lookup but can't efficiently find all words with a prefix — that would require scanning every word. A Trie navigates directly to the prefix node and collects results from the subtree.'
Sketch the Trie Node
'Each node has a children map (char → TrieNode) and an isEnd boolean. Optionally, I'll store the word itself at end nodes for easy retrieval.' — Drawing 3-4 nodes on the whiteboard makes the structure concrete.
Code Insert + the Core Operation
Write insert first (it's always the same). Then write the problem-specific operation: search, startsWith, wildcard DFS, or grid backtracking. Keep the Trie class separate from the solution logic.
Analyze Time & Space
'Insert: O(L) per word, O(n×L) total. Search/prefix: O(L). Space: O(n×L) worst case (no shared prefixes), but typically much less due to prefix sharing. For Word Search II: O(m×n×4^L) with Trie pruning making it practical.'
Common Follow-Up Questions
| Follow-Up | How to Handle |
|---|---|
| "Can you implement the Trie from scratch?" | Yes — this is expected at FAANG. Write the TrieNode class (children Map + isEnd) and the insert/search/startsWith methods. Practice until you can write it in under 3 minutes. |
| "How would you handle deletion?" | Walk to the word's end node, unmark isEnd. Optionally, walk back up and delete nodes that have no other children and aren't end-of-word. Use a recursive approach or track the path. |
| "What about memory optimization?" | Mention compressed Tries (radix trees): collapse single-child chains into one node with a string label. This reduces node count significantly. Also mention using array[26] vs Map tradeoff. |
| "Can you return the top-K suggestions sorted?" | Sort the words before inserting. DFS on the Trie naturally yields lexicographic order. Collect up to K results and stop. Alternatively, store a priority queue at each node (more complex). |
| "What if the alphabet is very large (Unicode)?" | Use a HashMap for children instead of an array. The Trie still works but uses more memory per node. For very large alphabets, consider a ternary search tree as an alternative. |
| "How does this compare to a hash-based approach?" | HashSet: O(L) exact lookup, O(n×L) for prefix scan. Trie: O(L) for both exact and prefix lookup. Trie wins when you have many prefix queries. HashSet wins for pure exact lookups (simpler, less memory). |
The meta-skill
Interviewers expect you to implement a Trie from scratch at FAANG-level interviews. Unlike heaps or balanced BSTs (where you use library implementations), Tries have no standard library in most languages. Practice writing the TrieNode + insert + search until it's muscle memory. The implementation is short — about 30 lines — but you need to write it confidently and quickly.
Summary & Cheat Sheet
Pin this section. Review it before mock interviews and contests.
Quick Revision Cheat Sheet
Core idea: A tree where each node is a character. Paths from root spell words/prefixes. Shared prefixes are stored once. O(L) for insert, search, and prefix lookup.
TrieNode: children: Map<string, TrieNode>, isEnd: boolean. Optionally: word (string), count (number).
Insert: Walk root to end, creating nodes as needed. Mark the last node isEnd = true.
Search: Walk root to end. Return node.isEnd (not just 'node exists').
StartsWith: Walk root to end of prefix. Return true if you reach the end without hitting null.
Autocomplete: Walk to prefix node, then DFS to collect all words in the subtree.
Wildcard (.): On '.', branch into ALL children via DFS. On a letter, follow that specific child.
Word Search II: Trie + grid backtracking. Walk Trie and grid simultaneously. Trie prunes invalid paths. Store word at end nodes.
Binary Trie: Children are 0 and 1. Insert numbers bit by bit (MSB first). Greedily pick opposite bit for max XOR.
Space tradeoff: Tries use more memory than HashSets. Worth it when you have many prefix queries. Mention compressed Tries for optimization.
Mental trigger: "Prefix", "autocomplete", "starts with", "word dictionary + grid", "replace with shortest root" → Trie
Decision Flowchart
Does the problem involve PREFIX-based operations on strings? ├── NO → Probably not a Trie problem. │ Exact lookup only? → HashSet │ Substring matching? → KMP, Rabin-Karp, or Suffix structures │ Sorting strings? → Radix sort or standard sort └── YES → What kind of prefix operation? ├── Insert + search + startsWith? │ └── Basic Trie (LC 208) ├── Find all words with a given prefix? │ └── Trie + DFS collection (autocomplete) ├── Search with wildcards (.)? │ └── Trie + recursive DFS branching (LC 211) ├── Find words from a dictionary in a grid? │ └── Trie + Backtracking (LC 212) ├── Replace words with shortest dictionary root? │ └── Trie walk, stop at first isEnd (LC 648) └── Maximum XOR of two numbers? └── Binary Trie (LC 421) Node structure needed? ├── Just isEnd → basic problems ├── isEnd + word string → Word Search II (easy retrieval) ├── isEnd + count → counting prefixes, Map Sum Pairs └── children[0] and children[1] → binary Trie for XOR
One last thing
The Trie is one of the few data structures you're expected to implement from scratch in FAANG interviews. The good news: it's only about 30 lines of code, and the structure is always the same. Master the basic implementation (problem 1), then layer on variations: wildcard DFS, grid backtracking, binary Trie. Solve the 7 practice problems in Section 07, and you'll recognize Trie problems instantly and implement them confidently.