The problem
Design a class Trie with three methods:
insert(word)stores a word.search(word)returnsTrueonly if that exact word was inserted.startsWith(prefix)returnsTrueif any inserted word begins withprefix.
All words use lowercase letters.
insert("apple")
search("apple") -> True
search("app") -> False
startsWith("app") -> True"app" is a prefix of a stored word, but nobody inserted "app" itself, so search says no and startsWith says yes.
How to think about it
Start with brute force. Keep the words in a list or a set. search is easy with a set, O(L) to hash a word of length L. But startsWith has to check every stored word, which is O(N·L) for N words. In a typeahead box that is called on every keystroke, that is far too slow.
The key insight: share the prefixes. Store words as paths in a tree where each edge is a character. apple and app share the nodes a → p → p. Now any operation only walks the characters of its argument, so it costs O(L), no matter how many words are stored.
Telling “word” from “prefix”. After walking a → p → p, how do you know whether app was inserted or is just part of apple? Put a sentinel key '$' in the node where a word ends. search needs the sentinel at the end of the walk; startsWith only needs the walk to succeed.
Which pattern is this? It is the trie itself, the base of every problem in this lesson. The tell is “prefix” or “starts with” over a set of words. In Python, a dict of dicts is the fastest trie to write: setdefault(c, {}) creates a child only if it is missing.
Approach
- Keep
root = {}. insert(w): start at root; for each character, step into that child, creating it if missing. At the end, set'$'.search(w): walk the characters. If a child is missing, returnFalse. At the end, return whether'$'is in the node.startsWith(p): the same walk, but returnTrueat the end without checking'$'.
Solution
class Trie:
def __init__(self): self.root = {}
def insert(self, w):
n = self.root
for c in w: n = n.setdefault(c, {}) # create child only if missing
n['$'] = True # mark a word end
def search(self, w):
n = self.root
for c in w:
if c not in n: return False
n = n[c]
return '$' in n # must be a full word
def startsWith(self, p):
n = self.root
for c in p:
if c not in n: return False
n = n[c]
return True # any path is enough
# insert("apple"); search("apple") -> True; search("app") -> False; startsWith("app") -> TrueDry run
Running the operations in order on a fresh Trie():
| Operation | Walk | End node has '$'? |
Result |
|---|---|---|---|
insert("apple") |
creates a→p→p→l→e | set it | — |
search("apple") |
a→p→p→l→e | yes | True |
search("app") |
a→p→p | no | False |
startsWith("app") |
a→p→p | not checked | True |
insert("app") |
reuses a→p→p | set it | — |
search("app") |
a→p→p | yes | True |
search("b") |
b missing at root |
— | False |
Notice that the second insert creates no new nodes. It only adds the end marker.
Complexity
- Time: O(L) per operation, where L is the length of the word or prefix. Each character is one dict lookup.
- Space: O(S) in total, where S is the total number of characters inserted. Shared prefixes reduce this in practice.
Edge cases and pitfalls
- Prefix vs word. The most common bug is returning
Truefromsearchwithout checking'$', which makessearch("app")wrong. - Empty prefix.
startsWith("")returnsTruebecause the walk never fails. That matches the usual definition. - Inserting twice is harmless: the path exists and
'$'is set again. - Sentinel clash.
'$'is safe because inputs are lowercase letters. If inputs could contain'$', use a class with anis_endfield instead.
Related problems
- Design Add and Search Words Data Structure (LeetCode 211): the same trie, with a
.wildcard in search. - Map Sum Pairs (LeetCode 677): a trie that answers a sum over every key with a prefix.
- Search Suggestions System (LeetCode 1268): typeahead, the classic real-world use of a trie.
- 1.Longest Common Prefix (LeetCode 14)
- 2.Implement Trie (Prefix Tree) (LeetCode 208)
- 3.Design Add and Search Words Data Structure (LeetCode 211)
- 4.Replace Words (LeetCode 648)
- 5.Map Sum Pairs (LeetCode 677)
- 6.Longest Word in Dictionary (LeetCode 720)
- 7.Search Suggestions System (LeetCode 1268)
- 8.Maximum XOR of Two Numbers in an Array (LeetCode 421)
- 9.Short Encoding of Words (LeetCode 820)
- 10.Word Search II (LeetCode 212)