When you hear a problem that talks about prefixes, autocomplete, dictionary lookup, or finding words that share a common start, the trie (or prefix‑tree) pattern is often the right tool. A trie turns a collection of strings into a tree where each edge represents a character and each path from the root to a node spells a prefix of some word. Because the structure shares common prefixes, operations that depend on the length of the query rather than the number of stored words become very fast.
What a Trie Looks Like
Imagine the words "cat", "car", and "dog". In a trie they appear as:
root
├─ c ─ a ─ t (word)
│ └─ r (word)
└─ d ─ o ─ g (word)
Each node may have up to σ children, where σ is the size of the alphabet (26 for lowercase English letters, 128 for ASCII, etc.). A boolean flag on a node tells us whether a word ends there.
Signals That Call for a Trie
| Problem wording | Why a trie helps |
|---|---|
| "Return all words that start with a given prefix" | Direct prefix lookup is O(L) where L is the prefix length. |
| "Find the longest common prefix of a set of strings" | Traversing the tree until a node has more than one child gives the answer in O(L). |
| "Count how many words differ by at most k characters" | A trie lets you explore only the relevant branches, pruning early. |
| "Implement autocomplete with a frequency ranking" | Store extra data (e.g., count) at each node and retrieve the top‑k completions efficiently. |
| "Detect if any word is a suffix of another" | Insert reversed words into a trie and look for prefix matches. |
If you see any of these patterns, pause and consider building a trie.
Worked Example: Autocomplete with Frequency
Below is a compact Python implementation that supports two operations:
insert(word, freq)– add a word with a usage count.autocomplete(prefix, k)– return up to k most frequent completions.
from collections import defaultdict
import heapq
class TrieNode:
__slots__ = ('children', 'freq', 'is_word')
def __init__(self):
self.children = {}
self.freq = 0 # cumulative frequency for this prefix
self.is_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str, freq: int = 1) -> None:
node = self.root
for ch in word:
node = node.children.setdefault(ch, TrieNode())
node.freq += freq # update prefix frequency
node.is_word = True
node.freq += freq # final node also stores its own freq
def _collect(self, node: TrieNode, prefix: str, heap: list, k: int) -> None:
if node.is_word:
# Use a min‑heap to keep only top‑k frequencies
heapq.heappush(heap, (node.freq, prefix))
if len(heap) > k:
heapq.heappop(heap)
for ch, child in node.children.items():
self._collect(child, prefix + ch, heap, k)
def autocomplete(self, prefix: str, k: int = 5):
node = self.root
for ch in prefix:
if ch not in node.children:
return []
node = node.children[ch]
heap = []
self._collect(node, prefix, heap, k)
# Return results sorted by descending frequency
return [word for _, word in sorted(heap, reverse=True)]
Complexity
- Insertion: O(L) where L = len(word). The extra frequency update is constant per character.
- Autocomplete: O(L + m log k) where m is the number of nodes visited under the prefix. The heap keeps the top‑k results, so we avoid sorting the entire subtree.
Why it works: By storing cumulative frequencies, we can prune branches that are unlikely to produce high‑rank completions, and the heap ensures we only keep the best k candidates.
Common Pitfalls
- Memory blow‑up – A naïve trie with a full array of size σ at each node wastes space when the alphabet is large. Use a dictionary (as in the example) or a compact array only for dense alphabets.
- Forgetting to mark word ends – Without an
is_wordflag you cannot distinguish a prefix from a complete word, leading to wrong answers for exact‑match queries. - Recursion depth – Deep recursion (
_collect) can hit Python’s recursion limit on very long words. Convert to an explicit stack if you expect words longer than a few hundred characters. - Updating frequencies incorrectly – Incrementing only the terminal node’s frequency will make the prefix‑frequency heuristic fail. Update every node on the path.
- Over‑engineering – If the dataset is tiny (e.g., < 100 words) a simple list scan may be faster and easier to code. Reserve the trie for scenarios where the asymptotic benefit matters.
Five Practice Problems (with hints)
- Prefix Search – Given a list of words and a prefix, return all matching words.
- Hint: Build a trie, walk down the prefix, then DFS to collect results.
- Longest Common Prefix of an Array – Find the longest string that is a prefix of every word in the array.
- Hint: Insert all words, then walk down from the root until a node has more than one child or is not a word.
- Word Break II – Return all sentences that can be formed by inserting spaces into a string, using a dictionary of valid words.
- Hint: Store the dictionary in a trie to prune invalid continuations early; use memoization for overlapping subproblems.
- Maximum XOR of Two Numbers in an Array – Find the maximum XOR value obtainable by any two numbers.
- Hint: Treat each number as a 32‑bit binary string and insert into a trie; then for each number, walk the trie preferring the opposite bit to maximize XOR.
- Minimum Unique Prefix – For each word, output the shortest prefix that uniquely identifies it among all words.
- Hint: After building the trie, the first node on a word’s path whose
freqequals 1 gives the unique prefix.
- Hint: After building the trie, the first node on a word’s path whose
These problems cover typical interview scenarios: direct prefix queries, combinatorial search with pruning, and bit‑wise tricks that reuse the same structural idea.
How to Practice This
- Implement the core trie – Write a class that supports
insert,search, andstartsWith. Test it with a handful of words before adding extras like frequency or deletion. - Solve one of the practice problems – Pick a problem, implement a brute‑force solution first, then replace the brute part with a trie and compare runtimes.
- Explain the solution aloud – Use Call Assistant to record yourself walking through the algorithm. Listening back helps you refine the story you’ll tell in an interview.
FAQ
When is a trie a bad choice? When the alphabet is huge and the dataset is small, the overhead of many nodes outweighs the prefix‑lookup benefit. In such cases a hash map or sorted list may be simpler.
Do I need to delete words from a trie? Deletion is rarely required in interviews, but if you must, decrement the frequency counters on the path and remove nodes that become unnecessary (no children and not a word).
How does a trie differ from a hash table for word lookup? A hash table gives O(1) average lookup for exact matches, but it cannot answer prefix queries without scanning many keys. A trie provides O(L) time for any prefix‑related operation, independent of the total number of stored words.
Can I use a trie for numeric data? Yes. Treat each digit (or bit) as a character and build the tree accordingly. This is the basis of the maximum XOR problem and other bit‑wise tricks.
Frequently asked questions
When should I choose a trie over a hash map?
Pick a trie when the problem asks for prefix‑based queries, autocomplete, or any operation that depends on the length of the query rather than the total number of stored items. A hash map is better for exact‑match lookups only.
What is the typical memory cost of a trie?
Memory scales with the total number of characters across all inserted strings plus overhead for child pointers. Using dictionaries for children keeps memory proportional to actual branching, which is usually far less than a full array of size σ per node.
How do I handle case‑insensitivity?
Normalize input strings (e.g., lower‑case) before inserting or searching, or store both cases in the same node by mapping characters to a canonical form.
Is recursion required for trie traversal?
Recursion is convenient for depth‑first search, but an explicit stack works just as well and avoids recursion‑limit issues on very deep tries.
#coding pattern#trie#interview prep#algorithm#python