Trie basics
In one line
A trie stores strings as paths of characters from a shared root, so words with the same prefix share nodes and any lookup costs O(L) for a string of length L, no matter how many words are stored.
What it is
A trie (also called a prefix tree, pronounced 'try') is a tree where each edge is one character. A word is stored as the path from the root down to a node, and that last node is marked 'end of word'. The root itself represents the empty string.
Words that start the same way share the same path. 'car', 'cart' and 'cat' all go through c then a, so the letters 'c' and 'a' are stored once. Each node keeps its children either in a hash map (char to node) or in a fixed array of 26 slots for lowercase English letters.
The key fact: insert, search and prefix check all take O(L) time, where L is the length of the string you are looking at. The number of words stored does not matter, which is what makes tries good for prefix questions that a hash set cannot answer.
Why interviewers ask
Interviewers ask about tries because 'Implement Trie' is a classic warm-up, and because tries are the right tool whenever a problem talks about prefixes: autocomplete, word dictionaries, word search on a board, longest common prefix and XOR maximisation. They test whether you can design a small node class, get the end-of-word flag right, and explain why a trie beats a hash set for prefix queries but costs more memory.
How it works
- 1Create a root node with an empty set of children and is_end = False.
- 2Insert a word: start at the root. For each character, follow the child for that character, creating a new node if it does not exist. After the last character, set is_end = True on the node you are on.
- 3Search for a word: walk the same way, but if a child is missing, return False immediately. If you reach the end, return the node's is_end flag, because the path may exist only as a prefix of a longer word.
- 4Prefix check (starts_with): walk the prefix; if you never hit a missing child, some stored word starts with it, so return True. You do not look at is_end.
- 5Delete (less common): walk to the word's last node and clear is_end; optionally remove nodes on the way back up that now have no children and are not the end of another word.
Worked example · Insert car, cat, cart, dog and run three lookups
- Start: root has no children.
- insert('car'): root has no 'c', create it; 'c' has no 'a', create it; 'a' has no 'r', create it; mark 'r' as end. Path: root -> c -> a -> r(end).
- insert('cat'): root -> c exists, c -> a exists, a has no 't', create it and mark it end. Node 'a' now has two children: r and t.
- insert('cart'): root -> c -> a -> r all exist; r has no 't', create it and mark end. 'r' stays marked end too, because 'car' is still a word.
- insert('dog'): root has no 'd', create d -> o -> g(end). Root now has two children: c and d. Total nodes: c, a, r, t, t, d, o, g = 8 (plus the root), for 13 characters inserted.
- search('ca'): walk c -> a, the path exists but 'a' has is_end = False, so return False.
- starts_with('ca'): same walk succeeds, so return True. search('cart'): walk c -> a -> r -> t, t has is_end = True, return True.
- search('cab'): at node 'a' there is no child 'b', so return False after 3 steps without looking at any other word.
Picture it
A trie is like a phone's contact list grouped by letters as you type: tap 'M', then 'a', and you only see the contacts under 'Ma'. Everyone named Mark, Maria and Matt shares the same first two taps, and you never scan the contacts that start with other letters.
Reference code
class TrieNode:
def __init__(self):
self.children = {} # char -> TrieNode
self.is_end = False # True if a word ends at this node
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for ch in word:
node = node.children.setdefault(ch, TrieNode())
node.is_end = True
def _walk(self, s):
node = self.root
for ch in s:
node = node.children.get(ch)
if node is None:
return None
return node
def search(self, word):
node = self._walk(word)
return node is not None and node.is_end
def starts_with(self, prefix):
return self._walk(prefix) is not NoneEach node holds a children dict and an is_end flag. insert creates missing nodes along the path, _walk follows an existing path or returns None, search needs the final node to be the end of a word, and starts_with only needs the path to exist. Every operation is O(L) for a string of length L.
Key patterns
Implement Trie (insert, search, startsWith)
When: The problem asks you to build the structure itself (LeetCode 208)
How: Node with children map and is_end flag; search checks is_end, startsWith does not.
Longest common prefix
When: Find the shared prefix of a list of strings
How: Insert all words, then walk from the root while the node has exactly one child and is not the end of a word.
Replace words with roots
When: Replace each word in a sentence by its shortest dictionary prefix (LeetCode 648)
How: Insert roots; for each word walk the trie and stop at the first node with is_end = True.
Counting prefixes
When: Asked how many words start with a prefix
How: Store a counter on every node, incremented on each insert that passes through it; the answer is the counter of the prefix's last node.
Real-world use
Search box autocomplete
Elasticsearch's completion suggester stores suggestions in an FST (finite state transducer), a compressed relative of the trie, so it can return completions for a typed prefix in a few milliseconds.
IP routing tables
Routers must find the longest matching prefix of a destination address. The Linux kernel's IPv4 routing table uses a level-compressed trie (fib_trie) over the bits of the address.
Redis internals
Redis uses a radix tree (a trie where single-child chains are merged into one edge), called rax, to store Redis Streams and other key sets compactly in memory.
Ethereum state
Ethereum stores account state in a Merkle Patricia Trie, a compressed trie with hashes, so any account lookup follows the key's path and the root hash commits to all state.
In a project
You are building a contacts or product search box in a mobile app that must show matches as the user types each letter, over 50,000 names stored on the device.
- Load all names (lowercased) into a trie once at app start, storing at each end node the original display name.
- On each keystroke, continue walking from the node you reached for the previous prefix instead of starting again from the root, so each new letter costs O(1).
- Collect a handful of matches under the current node with a short DFS and show them.
- If the user deletes a letter, keep a stack of visited nodes and step back one node.
Impact: Each keystroke is answered in well under a millisecond regardless of list size, compared with scanning 50,000 names with startswith on every letter, and the UI stays smooth on low-end phones.
Trade-offs
Use it when
- You need prefix queries: 'which words start with...', autocomplete, prefix counts
- You search many words that share prefixes, so shared nodes let you stop early
- You need sorted (alphabetical) iteration of words for free with a DFS
Avoid it when
- You only need exact lookups: a hash set is simpler and uses less memory
- Memory is tight and words share few prefixes: each node has per-node overhead (a dict or a 26-slot array)
- Keys are very long and random (hashes, UUIDs): the trie degenerates into long single-child chains; use a radix tree or a hash map
Cost: Insert, search and starts_with are O(L) time for a string of length L. Space is O(total characters inserted) nodes in the worst case; with 26-slot arrays that is up to 26 pointers per node.
Common mistakes
- Forgetting the is_end flag, so search('ca') returns True just because 'cat' exists.
- Setting is_end on the wrong node (the parent of the last character) or forgetting it entirely on insert.
- Saying lookup is O(n) in the number of words; it is O(L) in the length of the query.
- Using a 26-slot array without saying it assumes lowercase a-z; mixed input then crashes or needs a dict.
- Claiming tries always save memory: with few shared prefixes they use far more memory than a hash set.
Interview questions
Quick revision
- Trie = tree of characters; a word is a root-to-node path ending at a node with is_end = True.
- Insert, search, starts_with: O(L) time, independent of the number of words.
- search checks is_end; starts_with does not.
- Children: dict (any alphabet, sparse) or array of 26 (fast, lowercase only).
- Space: up to one node per inserted character; heavy per-node overhead.
- DFS over a trie visits words in alphabetical order (with sorted children).
- Radix tree / Patricia trie = trie with single-child chains merged.
Check yourself
1. You insert 'apple' into an empty trie. What does search('app') return?
2. A trie holds 1 million words. What is the time to check if a 6-letter word exists?
3. Which task is a hash set NOT good at but a trie handles well?
4. Why might a trie use more memory than a hash set for the same words?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.