Implement Trie (Prefix Tree) (LeetCode 208)
A trie ("try") or prefix tree is a tree structure purpose-built for storing and looking up a set of strings efficiently, and it underlies things like autocomplete and spellcheckers.
Implement a Trie class:
Trie()starts with an empty trie.void insert(String word)addswordto the trie.boolean search(String word)returnstrueonly ifworditself was previously inserted.boolean startsWith(String prefix)returnstrueif any inserted word begins withprefix.
Worked Examples
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]][null, null, true, false, true, null, true]"apple", search("apple") is true, search("app") is false (only a prefix so far), and startsWith("app") is true. After inserting "app", search("app") is true.⚖️Formal Constraints & Bounds
1 <= word.length, prefix.length <= 2000wordandprefixconsist only of lowercase English letters.At most
3 * 104calls in total will be made toinsert,search, andstartsWith.
Core Insight
Each tree node has 26 possible children and an is_end_of_word boolean flag. Lookup time is strictly proportional to word length L, independent of dictionary size N.
Real-World Scenario & Production Applications
Search engine autocomplete (e.g. Google search query prefix recommendations) and IP routing prefix lookup tables (CIDR) are powered by Tries.
Step-by-Step Execution Trace Table
| Step | Invocation | Path Walked | Node Landed On | is_end Checked | Output Value | Pedagogical Reason |
|---|---|---|---|---|---|---|
| 1 | insert("apple") | Creates a → p → p → l → e | e node | Set to True | — | Allocates 5 nodes; marks 'e' as end. |
| 2 | search("apple") | Traverses a → p → p → l → e | e node | True | True | Path exists AND is_end == True. |
| 3 | search("app") | Traverses a → p → p | Second p node | False | False | Path exists BUT is_end == False. |
| 4 | startsWith("app") | Traverses a → p → p | Second p node | Not checked | True | Path exists (prefix matches!). |
| 5 | insert("app") | Walks existing a → p → p | Second p node | Set to True | — | Zero new nodes allocated; flips is_end. |
| 6 | search("app") | Traverses a → p → p | Second p node | True | True | Path exists AND is_end == True. |
| 1 | each TrieNode holds children = {} and is_end = False |
| 2 | def _walk(s): |
| 3 | follow node.children[ch] for each ch, returning None at a missing child |
| 4 | return the last node |
| 5 | def insert(word): |
| 6 | node = node.children.setdefault(ch, TrieNode()) for each ch |
| 7 | node.is_end = True on the last node |
| 8 | def search(word): |
| 9 | node = self._walk(word); return node is not None and node.is_end |
| 10 | def startsWith(prefix): |
| 11 | return self._walk(prefix) is not None |
Conceptual Narrative
Storing "cat", "cats", and "category" as independent strings triplicates the common prefix "cat". A Trie collapses identical prefixes into a single shared path, turning prefix queries into linear descents.
🧭 Conceptual Foundation & Pattern Intuition
A hash set can verify if a string exists in average time, but answering startsWith(prefix) requires either hashing all possible substrings of every word or scanning the entire dictionary in time. A Trie (from re-TRIE-val) structures strings as a 26-ary tree where:
- Each node represents a single character transition.
- The root represents the empty string
"". - All words sharing a common prefix of length traverse the exact same sequence of nodes.
- An
is_endboolean flag distinguishes complete stored words from intermediate prefixes. Because descent depends solely on the characters of the query string, all operations run in strict time, regardless of whether the dictionary contains 10 words or 10,000,000 words.
🏛️ The Analogy: Walking a Numbered Library Hallway
Imagine entering an archive where every corridor branches into 26 numbered doors labeled 'a' through 'z'. To find the book "apple", you walk through door 'a', then down that hallway to door 'p', then 'p', 'l', and 'e'. At room 'e', a green bookmark rests on the table (is_end = True), confirming "apple" is a complete cataloged book. If someone asks if any book starts with "app", you only walk to door 'p' and verify the hallway continues—you never need to inspect the individual titles shelved further down the hall.
🔑 Why It Works
The elegance is that search and startsWith share 100% of their tree-walk mechanics through _walk; search merely requires node.is_end at the termination node, whereas startsWith returns True as long as the path does not hit a dead end.
Conflating path existence with word completion:
searchmust verifynode.is_end == True. Checking only that the character chain exists causes prefix-only matches to incorrectly returnTrue.Mutating state during search via
defaultdict: Usingdefaultdictorsetdefaultinsidesearch()orstartsWith()silently creates empty nodes for non-existent characters, corrupting prefix lookups on subsequent queries.Premature
is_endflag assignment: Settingis_end = Trueon the parent node before traversing to the child corresponding to the final character marks an incomplete word as stored.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
Word dictionary + autocomplete prefix searching.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: every root-to-node path represents a unique character prefix.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
searchreturnsnode is not None and node.is_end: without theis_endcheck,search("app")is true after inserting "apple".Using Python
defaultdictfor children in search: Accessingnode.children[ch]with adefaultdictsilently inserts empty nodes during a read-only search orstartsWithquery, corrupting trie structure and bloating memory.Memory overhead for sparse tries: Storing fixed-size 26-element arrays for unicode or sparse alphabets wastes space; use a hash table
children = {}or radix tree when alphabet size is large or sparse.
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use a Trie. Each node has a dict of children and an
is_endflag.insertwalks the word, creates missing children, then setsis_endon the last node.searchandstartsWithshare one walk that stops as soon as a child is missing, so reads never add nodes. This is correct because a path exists exactly when some inserted word has that prefix. The trap is thatsearchmust also checkis_end, orsearch("app")is true after inserting "apple";startsWithskips that check, because any path at all proves that some word begins with the prefix. Each call is O(L) time, L = word length, and the trie takes one node per distinct prefix, O(total characters) space."
Complexity & Mathematical Proof
O(L) per operation, L = word length
Look at the code: insert loops over the word once, and search and startsWith each call _walk, which loops over the word or prefix once. Every loop step is one dict lookup or one setdefault, O(1). So each call is O(L), however many words are stored.
O(total characters)
Look at memory allocations: insert and _walk are loops, so the call stack is O(1). The trie holds one TrieNode per distinct prefix of the inserted words, and each node's children dict holds only the edges that exist. One insert adds at most L new nodes, so all nodes together number at most the total characters of the inserted words: O(total characters).
T(L) = L transitions = O(L)
Derivation Progression
node = node.children[ch]
Follows one child per character, an O(1) dict lookup.
node.children.setdefault(ch, TrieNode())
insert creates a node only when the child is missing, so words with a common prefix reuse its nodes.
O(L) where L = len(word)
Operations are independent of total dictionary size N.
Variable Definitions
Length of the word or prefix passed to one call
Total number of words stored in Trie
Sum of the lengths of all inserted words
Memory Architecture & Bounds
O(1) Iterative
O(total characters): one TrieNode per distinct prefix
O(1) Boolean return
Boundary Best / Worst Cases
on early mismatch in search.
.
where L is word length.
Recurrence Tree Topology
Senior SWE Deconstruction & Hardware Caveats
Triggers: "prefix tree", "startsWith(prefix)", "search(word)". Hash sets cannot answer prefix membership without inspecting every candidate string. A Senior SWE immediately applies a Trie (Prefix Tree): an alphabet-branching tree where all strings sharing a common prefix traverse the exact same path from the root.
Words consist of lowercase English letters (), length , up to total operations. Time budget is strictly per insert, search, and prefix query, independent of the number of words stored ().
Deep paths: a single word of length 2000 builds a 2000-node chain, so a recursive walk or delete can hit Python's default recursion limit; keep insert and _walk as loops. Memory per node: every node is an object plus a dict, which costs far more than the one character it stands for, so a large dictionary may need a fixed child array or a compressed (radix) trie that stores a run of single-child nodes as one edge.
Implement Trie (Prefix Tree) (LeetCode 208)
A trie ("try") or prefix tree is a tree structure purpose-built for storing and looking up a set of strings efficiently, and it underlies things like autocomplete and spellcheckers.
Implement a Trie class:
Trie()starts with an empty trie.void insert(String word)addswordto the trie.boolean search(String word)returnstrueonly ifworditself was previously inserted.boolean startsWith(String prefix)returnstrueif any inserted word begins withprefix.
Worked Examples
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]][null, null, true, false, true, null, true]"apple", search("apple") is true, search("app") is false (only a prefix so far), and startsWith("app") is true. After inserting "app", search("app") is true.⚖️Formal Constraints & Bounds
1 <= word.length, prefix.length <= 2000wordandprefixconsist only of lowercase English letters.At most
3 * 104calls in total will be made toinsert,search, andstartsWith.
Core Insight
Each tree node has 26 possible children and an is_end_of_word boolean flag. Lookup time is strictly proportional to word length L, independent of dictionary size N.
Real-World Scenario & Production Applications
Search engine autocomplete (e.g. Google search query prefix recommendations) and IP routing prefix lookup tables (CIDR) are powered by Tries.
Step-by-Step Execution Trace Table
| Step | Invocation | Path Walked | Node Landed On | is_end Checked | Output Value | Pedagogical Reason |
|---|---|---|---|---|---|---|
| 1 | insert("apple") | Creates a → p → p → l → e | e node | Set to True | — | Allocates 5 nodes; marks 'e' as end. |
| 2 | search("apple") | Traverses a → p → p → l → e | e node | True | True | Path exists AND is_end == True. |
| 3 | search("app") | Traverses a → p → p | Second p node | False | False | Path exists BUT is_end == False. |
| 4 | startsWith("app") | Traverses a → p → p | Second p node | Not checked | True | Path exists (prefix matches!). |
| 5 | insert("app") | Walks existing a → p → p | Second p node | Set to True | — | Zero new nodes allocated; flips is_end. |
| 6 | search("app") | Traverses a → p → p | Second p node | True | True | Path exists AND is_end == True. |
| 1 | each TrieNode holds children = {} and is_end = False |
| 2 | def _walk(s): |
| 3 | follow node.children[ch] for each ch, returning None at a missing child |
| 4 | return the last node |
| 5 | def insert(word): |
| 6 | node = node.children.setdefault(ch, TrieNode()) for each ch |
| 7 | node.is_end = True on the last node |
| 8 | def search(word): |
| 9 | node = self._walk(word); return node is not None and node.is_end |
| 10 | def startsWith(prefix): |
| 11 | return self._walk(prefix) is not None |
Conceptual Narrative
Storing "cat", "cats", and "category" as independent strings triplicates the common prefix "cat". A Trie collapses identical prefixes into a single shared path, turning prefix queries into linear descents.
🧭 Conceptual Foundation & Pattern Intuition
A hash set can verify if a string exists in average time, but answering startsWith(prefix) requires either hashing all possible substrings of every word or scanning the entire dictionary in time. A Trie (from re-TRIE-val) structures strings as a 26-ary tree where:
- Each node represents a single character transition.
- The root represents the empty string
"". - All words sharing a common prefix of length traverse the exact same sequence of nodes.
- An
is_endboolean flag distinguishes complete stored words from intermediate prefixes. Because descent depends solely on the characters of the query string, all operations run in strict time, regardless of whether the dictionary contains 10 words or 10,000,000 words.
🏛️ The Analogy: Walking a Numbered Library Hallway
Imagine entering an archive where every corridor branches into 26 numbered doors labeled 'a' through 'z'. To find the book "apple", you walk through door 'a', then down that hallway to door 'p', then 'p', 'l', and 'e'. At room 'e', a green bookmark rests on the table (is_end = True), confirming "apple" is a complete cataloged book. If someone asks if any book starts with "app", you only walk to door 'p' and verify the hallway continues—you never need to inspect the individual titles shelved further down the hall.
🔑 Why It Works
The elegance is that search and startsWith share 100% of their tree-walk mechanics through _walk; search merely requires node.is_end at the termination node, whereas startsWith returns True as long as the path does not hit a dead end.
Conflating path existence with word completion:
searchmust verifynode.is_end == True. Checking only that the character chain exists causes prefix-only matches to incorrectly returnTrue.Mutating state during search via
defaultdict: Usingdefaultdictorsetdefaultinsidesearch()orstartsWith()silently creates empty nodes for non-existent characters, corrupting prefix lookups on subsequent queries.Premature
is_endflag assignment: Settingis_end = Trueon the parent node before traversing to the child corresponding to the final character marks an incomplete word as stored.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
Word dictionary + autocomplete prefix searching.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: every root-to-node path represents a unique character prefix.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
searchreturnsnode is not None and node.is_end: without theis_endcheck,search("app")is true after inserting "apple".Using Python
defaultdictfor children in search: Accessingnode.children[ch]with adefaultdictsilently inserts empty nodes during a read-only search orstartsWithquery, corrupting trie structure and bloating memory.Memory overhead for sparse tries: Storing fixed-size 26-element arrays for unicode or sparse alphabets wastes space; use a hash table
children = {}or radix tree when alphabet size is large or sparse.
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use a Trie. Each node has a dict of children and an
is_endflag.insertwalks the word, creates missing children, then setsis_endon the last node.searchandstartsWithshare one walk that stops as soon as a child is missing, so reads never add nodes. This is correct because a path exists exactly when some inserted word has that prefix. The trap is thatsearchmust also checkis_end, orsearch("app")is true after inserting "apple";startsWithskips that check, because any path at all proves that some word begins with the prefix. Each call is O(L) time, L = word length, and the trie takes one node per distinct prefix, O(total characters) space."
Complexity & Mathematical Proof
O(L) per operation, L = word length
Look at the code: insert loops over the word once, and search and startsWith each call _walk, which loops over the word or prefix once. Every loop step is one dict lookup or one setdefault, O(1). So each call is O(L), however many words are stored.
O(total characters)
Look at memory allocations: insert and _walk are loops, so the call stack is O(1). The trie holds one TrieNode per distinct prefix of the inserted words, and each node's children dict holds only the edges that exist. One insert adds at most L new nodes, so all nodes together number at most the total characters of the inserted words: O(total characters).
T(L) = L transitions = O(L)
Derivation Progression
node = node.children[ch]
Follows one child per character, an O(1) dict lookup.
node.children.setdefault(ch, TrieNode())
insert creates a node only when the child is missing, so words with a common prefix reuse its nodes.
O(L) where L = len(word)
Operations are independent of total dictionary size N.
Variable Definitions
Length of the word or prefix passed to one call
Total number of words stored in Trie
Sum of the lengths of all inserted words
Memory Architecture & Bounds
O(1) Iterative
O(total characters): one TrieNode per distinct prefix
O(1) Boolean return
Boundary Best / Worst Cases
on early mismatch in search.
.
where L is word length.
Recurrence Tree Topology
Senior SWE Deconstruction & Hardware Caveats
Triggers: "prefix tree", "startsWith(prefix)", "search(word)". Hash sets cannot answer prefix membership without inspecting every candidate string. A Senior SWE immediately applies a Trie (Prefix Tree): an alphabet-branching tree where all strings sharing a common prefix traverse the exact same path from the root.
Words consist of lowercase English letters (), length , up to total operations. Time budget is strictly per insert, search, and prefix query, independent of the number of words stored ().
Deep paths: a single word of length 2000 builds a 2000-node chain, so a recursive walk or delete can hit Python's default recursion limit; keep insert and _walk as loops. Memory per node: every node is an object plus a dict, which costs far more than the one character it stands for, so a large dictionary may need a fixed child array or a compressed (radix) trie that stores a run of single-child nodes as one edge.