61. Implement Trie (Prefix Tree)
View on NeetCode
View on LeetCode
Problem
A trie (pronounced as “try”) or prefix tree is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. There are various applications of this data structure, such as autocomplete and spellchecker.
Implement the Trie class:
Trie()Initializes the trie object.void insert(String word)Inserts the stringwordinto the trie.boolean search(String word)Returnstrueif the stringwordis in the trie (i.e., was inserted before), andfalseotherwise.boolean startsWith(String prefix)Returnstrueif there is a previously inserted stringwordthat has the prefixprefix, andfalseotherwise.
Example 1:
Input:
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
Output:
[null, null, true, false, true, null, true]
Explanation:
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // return True
trie.search("app"); // return False
trie.startsWith("app"); // return True
trie.insert("app");
trie.search("app"); // return True
Constraints:
1 <= word.length, prefix.length <= 2000wordandprefixconsist only of lowercase English letters.- At most
3 * 10^4calls in total will be made toinsert,search, andstartsWith.
Solution
Approach: Trie Node with Children Map
The key insight is that each node stores a map of children (character → child node) and a flag indicating if it’s the end of a word.
Implementation
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def search(self, word: str) -> bool:
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word
def startsWith(self, prefix: str) -> bool:
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
Alternative (Using Array for Children):
class TrieNode:
def __init__(self):
self.children = [None] * 26 # For lowercase a-z
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for char in word:
idx = ord(char) - ord('a')
if not node.children[idx]:
node.children[idx] = TrieNode()
node = node.children[idx]
node.is_end_of_word = True
def search(self, word: str) -> bool:
node = self.root
for char in word:
idx = ord(char) - ord('a')
if not node.children[idx]:
return False
node = node.children[idx]
return node.is_end_of_word
def startsWith(self, prefix: str) -> bool:
node = self.root
for char in prefix:
idx = ord(char) - ord('a')
if not node.children[idx]:
return False
node = node.children[idx]
return True
Complexity Analysis
- Time Complexity:
insert: O(m), where m is the length of the wordsearch: O(m)startsWith: O(m)
- Space Complexity: O(n * m), where n is the number of words and m is the average word length.
Key Insights
-
Trie Structure: Each node represents a character, and paths from root to nodes spell out prefixes/words.
-
End of Word Marker: We need
is_end_of_wordto distinguish between “app” (not inserted) and “apple” (inserted) when both share the prefix “app”. -
Shared Prefixes: Multiple words sharing a prefix share the same nodes for that prefix, saving space.
-
Search vs StartsWith: The only difference is that
searchchecksis_end_of_wordwhilestartsWithdoesn’t care. -
Children Storage: Using a hash map is more space-efficient for sparse character sets, while an array is faster for dense character sets (all lowercase letters).