208. Implement Trie (Prefix Tree)
Problem Description
This problem asks you to implement a Trie (also known as a prefix tree), which is a tree-like data structure designed for efficient storage and retrieval of strings.
You need to create a Trie
class with the following operations:
-
Trie()
: Constructor that initializes an empty trie. -
insert(word)
: Takes a stringword
and adds it to the trie. After insertion, the word should be stored in the trie structure. -
search(word)
: Takes a stringword
and checks if the exact word exists in the trie. Returnstrue
if the word was previously inserted,false
otherwise. -
startsWith(prefix)
: Takes a stringprefix
and checks if any word in the trie starts with this prefix. Returnstrue
if at least one previously inserted word begins with the given prefix,false
otherwise.
The key distinction between search
and startsWith
is that search
looks for complete words that were inserted, while startsWith
only checks if the given string is a prefix of any inserted word.
For example:
- After inserting "apple",
search("apple")
returnstrue
search("app")
returnsfalse
(since "app" was never inserted as a complete word)startsWith("app")
returnstrue
(since "app" is a prefix of "apple")
The implementation uses a tree structure where each node can have up to 26 children (one for each lowercase letter a-z), and each node has a boolean flag is_end
to mark whether it represents the end of a complete word.
Intuition
Think about how we naturally organize words with common prefixes - like in a dictionary where "cat", "cats", and "category" are grouped together because they share the prefix "cat". This natural grouping suggests a tree structure where common prefixes share the same path from the root.
The key insight is that we can represent each character as a branch in our tree. Starting from the root, we follow a path down the tree for each character in a word. For example, inserting "cat" would create a path: root β c β a β t.
Since we're dealing with lowercase English letters only, each node needs at most 26 children (one for each letter a-z). We can use an array of size 26 where children[0]
represents 'a', children[1]
represents 'b', and so on. This gives us O(1) access to any child node by calculating the index as ord(character) - ord('a')
.
The challenge is distinguishing between a complete word and just a prefix. For instance, if we insert both "cat" and "category", the path for "cat" is contained within the path for "category". We need a way to mark that "cat" is a complete word, not just a prefix. This is where the is_end
flag comes in - we set it to True
at the node representing the last character of each inserted word.
For searching, we traverse the tree following the characters of the target word. If we can follow the complete path and the final node has is_end = True
, we've found the word. For prefix checking, we only need to verify that we can follow the path for all characters in the prefix - we don't care about the is_end
flag.
This approach naturally handles the shared prefix problem efficiently. Instead of storing "cat", "cats", and "category" as three separate strings (which would duplicate the "cat" prefix three times), we store the shared prefix once and branch out only where the words differ.
Solution Approach
The implementation uses a tree structure where each node contains an array of 26 child pointers and a boolean flag to mark word endings.
Node Structure:
Each Trie
node has:
self.children
: An array of size 26, initialized withNone
values. Each index corresponds to a letter ('a' at index 0, 'b' at index 1, etc.)self.is_end
: A boolean flag set toFalse
initially, marking whether this node represents the end of a complete word
Insert Operation:
def insert(self, word: str) -> None:
node = self
for c in word:
idx = ord(c) - ord('a')
if node.children[idx] is None:
node.children[idx] = Trie()
node = node.children[idx]
node.is_end = True
Starting from the root, we traverse character by character:
- Calculate the index for each character using
ord(c) - ord('a')
(converts 'a' to 0, 'b' to 1, etc.) - If no child exists at that index, create a new
Trie
node - Move to the child node and continue with the next character
- After processing all characters, mark the final node with
is_end = True
Helper Method - Search Prefix:
def _search_prefix(self, prefix: str):
node = self
for c in prefix:
idx = ord(c) - ord('a')
if node.children[idx] is None:
return None
node = node.children[idx]
return node
This helper method traverses the trie following the path of the given prefix:
- For each character, calculate its index and check if a child exists
- If any character doesn't have a corresponding child, return
None
(prefix doesn't exist) - If all characters are found, return the final node reached
Search Operation:
def search(self, word: str) -> bool:
node = self._search_prefix(word)
return node is not None and node.is_end
To search for a complete word:
- Use
_search_prefix
to find the node representing the last character - Return
True
only if the node exists ANDis_end
isTrue
(confirming it's a complete word, not just a prefix)
StartsWith Operation:
def startsWith(self, prefix: str) -> bool:
node = self._search_prefix(prefix)
return node is not None
To check if any word starts with the given prefix:
- Use
_search_prefix
to traverse the prefix path - Return
True
if we can successfully traverse all characters (node is notNone
) - We don't check
is_end
because we only care that the prefix exists, not whether it's a complete word
This implementation achieves O(m) time complexity for all operations where m is the length of the word/prefix, and uses O(ALPHABET_SIZE Γ N Γ m) space in the worst case, where N is the number of words inserted.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the Trie works:
Example Operations:
- Insert "cat"
- Insert "car"
- Search "cat" β returns True
- Search "ca" β returns False
- StartsWith "ca" β returns True
Step-by-step visualization:
Initial State: Empty trie with just a root node
root (is_end=False) children: [None] * 26
Step 1: Insert "cat"
- Process 'c': idx = 2, create child at index 2
- Process 'a': idx = 0, create child at index 0
- Process 't': idx = 19, create child at index 19
- Mark the 't' node with is_end = True
root β [c] β [a] β [t] (is_end=True)
Step 2: Insert "car"
- Process 'c': idx = 2, child exists, move to it
- Process 'a': idx = 0, child exists, move to it (shared prefix!)
- Process 'r': idx = 17, create child at index 17
- Mark the 'r' node with is_end = True
root | [c] | [a] / \ [t] [r] (end) (end)
Step 3: Search "cat"
- Call
_search_prefix("cat")
:- 'c': children[2] exists, move to it
- 'a': children[0] exists, move to it
- 't': children[19] exists, move to it
- Returns the 't' node
- Check: node exists AND node.is_end = True
- Result: True β
Step 4: Search "ca"
- Call
_search_prefix("ca")
:- 'c': children[2] exists, move to it
- 'a': children[0] exists, move to it
- Returns the 'a' node
- Check: node exists BUT node.is_end = False
- Result: False β (it's only a prefix, not a complete word)
Step 5: StartsWith "ca"
- Call
_search_prefix("ca")
:- 'c': children[2] exists, move to it
- 'a': children[0] exists, move to it
- Returns the 'a' node (not None)
- Check: node is not None
- Result: True β (prefix exists in the trie)
Key Observations:
- "cat" and "car" share the prefix "ca", so they share the same path for those characters
- The is_end flag distinguishes complete words from prefixes
- Each character lookup is O(1) using array indexing
- The trie efficiently reuses common prefixes, saving space
Solution Implementation
1class Trie:
2 """
3 A Trie (prefix tree) data structure implementation for efficient string operations.
4 Each node contains an array of 26 children (for lowercase English letters a-z)
5 and a boolean flag to mark word endings.
6 """
7
8 def __init__(self):
9 """Initialize the Trie node with empty children array and end-of-word flag."""
10 self.children = [None] * 26 # Array to store 26 lowercase letters
11 self.is_end_of_word = False # Flag to mark if this node represents end of a word
12
13 def insert(self, word: str) -> None:
14 """
15 Insert a word into the Trie.
16 Time Complexity: O(n) where n is the length of the word
17 Space Complexity: O(n) in worst case when creating new nodes
18
19 Args:
20 word: The word to be inserted into the Trie
21 """
22 current_node = self
23
24 for char in word:
25 # Calculate the index for current character (0-25 for a-z)
26 char_index = ord(char) - ord('a')
27
28 # Create new node if path doesn't exist
29 if current_node.children[char_index] is None:
30 current_node.children[char_index] = Trie()
31
32 # Move to the child node
33 current_node = current_node.children[char_index]
34
35 # Mark the last node as end of word
36 current_node.is_end_of_word = True
37
38 def search(self, word: str) -> bool:
39 """
40 Search for a complete word in the Trie.
41
42 Args:
43 word: The word to search for
44
45 Returns:
46 True if the word exists in the Trie, False otherwise
47 """
48 final_node = self._search_prefix(word)
49 return final_node is not None and final_node.is_end_of_word
50
51 def startsWith(self, prefix: str) -> bool:
52 """
53 Check if any word in the Trie starts with the given prefix.
54
55 Args:
56 prefix: The prefix to search for
57
58 Returns:
59 True if any word starts with the prefix, False otherwise
60 """
61 prefix_node = self._search_prefix(prefix)
62 return prefix_node is not None
63
64 def _search_prefix(self, prefix: str) -> 'Trie':
65 """
66 Helper method to traverse the Trie following the given prefix.
67
68 Args:
69 prefix: The prefix string to traverse
70
71 Returns:
72 The Trie node at the end of the prefix path, or None if path doesn't exist
73 """
74 current_node = self
75
76 for char in prefix:
77 # Calculate the index for current character
78 char_index = ord(char) - ord('a')
79
80 # Return None if path doesn't exist
81 if current_node.children[char_index] is None:
82 return None
83
84 # Move to the child node
85 current_node = current_node.children[char_index]
86
87 return current_node
88
89
90# Your Trie object will be instantiated and called as such:
91# obj = Trie()
92# obj.insert(word)
93# param_2 = obj.search(word)
94# param_3 = obj.startsWith(prefix)
95
1/**
2 * Trie (Prefix Tree) implementation for efficient string storage and retrieval.
3 * Supports insertion, exact word search, and prefix search operations.
4 * Assumes all inputs contain only lowercase English letters 'a' to 'z'.
5 */
6class Trie {
7 // Array to store child nodes for each letter (a-z)
8 private Trie[] children;
9
10 // Flag to mark if current node represents end of a word
11 private boolean isEndOfWord;
12
13 /**
14 * Constructor initializes an empty Trie node.
15 * Creates an array of 26 slots for lowercase English letters.
16 */
17 public Trie() {
18 children = new Trie[26];
19 isEndOfWord = false;
20 }
21
22 /**
23 * Inserts a word into the Trie.
24 * Time Complexity: O(n) where n is the length of the word.
25 *
26 * @param word The word to be inserted into the Trie
27 */
28 public void insert(String word) {
29 Trie currentNode = this;
30
31 // Traverse through each character in the word
32 for (char character : word.toCharArray()) {
33 // Calculate the index for the character (0 for 'a', 1 for 'b', etc.)
34 int index = character - 'a';
35
36 // Create a new node if the path doesn't exist
37 if (currentNode.children[index] == null) {
38 currentNode.children[index] = new Trie();
39 }
40
41 // Move to the child node
42 currentNode = currentNode.children[index];
43 }
44
45 // Mark the last node as end of the word
46 currentNode.isEndOfWord = true;
47 }
48
49 /**
50 * Searches for an exact word in the Trie.
51 * Time Complexity: O(n) where n is the length of the word.
52 *
53 * @param word The word to search for
54 * @return true if the word exists in the Trie, false otherwise
55 */
56 public boolean search(String word) {
57 Trie prefixEndNode = searchPrefix(word);
58
59 // Word exists only if we found the prefix AND it's marked as end of word
60 return prefixEndNode != null && prefixEndNode.isEndOfWord;
61 }
62
63 /**
64 * Checks if any word in the Trie starts with the given prefix.
65 * Time Complexity: O(n) where n is the length of the prefix.
66 *
67 * @param prefix The prefix to search for
68 * @return true if any word starts with the prefix, false otherwise
69 */
70 public boolean startsWith(String prefix) {
71 Trie prefixEndNode = searchPrefix(prefix);
72
73 // Prefix exists if we can traverse the entire prefix path
74 return prefixEndNode != null;
75 }
76
77 /**
78 * Helper method to search for a prefix in the Trie.
79 * Returns the node where the prefix ends, or null if prefix doesn't exist.
80 *
81 * @param prefix The prefix string to search for
82 * @return The Trie node at the end of the prefix path, or null if not found
83 */
84 private Trie searchPrefix(String prefix) {
85 Trie currentNode = this;
86
87 // Traverse through each character in the prefix
88 for (char character : prefix.toCharArray()) {
89 // Calculate the index for the character
90 int index = character - 'a';
91
92 // If path doesn't exist, prefix is not in the Trie
93 if (currentNode.children[index] == null) {
94 return null;
95 }
96
97 // Move to the child node
98 currentNode = currentNode.children[index];
99 }
100
101 // Return the node where the prefix ends
102 return currentNode;
103 }
104}
105
106/**
107 * Your Trie object will be instantiated and called as such:
108 * Trie obj = new Trie();
109 * obj.insert(word);
110 * boolean param_2 = obj.search(word);
111 * boolean param_3 = obj.startsWith(prefix);
112 */
113
1class Trie {
2private:
3 // Array to store pointers to child nodes (26 for lowercase letters a-z)
4 vector<Trie*> children;
5 // Flag to mark if current node represents end of a word
6 bool isEnd;
7
8 /**
9 * Helper function to search for a prefix in the trie
10 * @param prefix The string prefix to search for
11 * @return Pointer to the node representing the last character of prefix, or nullptr if not found
12 */
13 Trie* searchPrefix(string prefix) {
14 Trie* currentNode = this;
15
16 // Traverse through each character in the prefix
17 for (char ch : prefix) {
18 int index = ch - 'a'; // Convert character to array index (0-25)
19
20 // If the child node doesn't exist, prefix not found
21 if (!currentNode->children[index]) {
22 return nullptr;
23 }
24
25 // Move to the child node
26 currentNode = currentNode->children[index];
27 }
28
29 return currentNode;
30 }
31
32public:
33 /**
34 * Constructor: Initialize the trie with empty children array and isEnd flag
35 */
36 Trie() : children(26), isEnd(false) {}
37
38 /**
39 * Insert a word into the trie
40 * @param word The word to insert
41 */
42 void insert(string word) {
43 Trie* currentNode = this;
44
45 // Traverse through each character in the word
46 for (char ch : word) {
47 int index = ch - 'a'; // Convert character to array index (0-25)
48
49 // Create new node if path doesn't exist
50 if (!currentNode->children[index]) {
51 currentNode->children[index] = new Trie();
52 }
53
54 // Move to the child node
55 currentNode = currentNode->children[index];
56 }
57
58 // Mark the last node as end of word
59 currentNode->isEnd = true;
60 }
61
62 /**
63 * Search for a complete word in the trie
64 * @param word The word to search for
65 * @return true if the word exists in the trie, false otherwise
66 */
67 bool search(string word) {
68 Trie* targetNode = searchPrefix(word);
69
70 // Word exists only if we found the prefix AND it's marked as end of word
71 return targetNode != nullptr && targetNode->isEnd;
72 }
73
74 /**
75 * Check if any word in the trie starts with the given prefix
76 * @param prefix The prefix to search for
77 * @return true if any word starts with the prefix, false otherwise
78 */
79 bool startsWith(string prefix) {
80 Trie* targetNode = searchPrefix(prefix);
81
82 // Prefix exists if we can find a path for all its characters
83 return targetNode != nullptr;
84 }
85};
86
87/**
88 * Your Trie object will be instantiated and called as such:
89 * Trie* obj = new Trie();
90 * obj->insert(word);
91 * bool param_2 = obj->search(word);
92 * bool param_3 = obj->startsWith(prefix);
93 */
94
1// TrieNode structure to represent each node in the Trie
2interface TrieNode {
3 children: (TrieNode | null)[];
4 isEndOfWord: boolean;
5}
6
7// Root node of the Trie data structure
8let trieRoot: TrieNode;
9
10// Initialize the Trie with an empty root node
11function Trie(): void {
12 trieRoot = {
13 children: new Array(26).fill(null),
14 isEndOfWord: false
15 };
16}
17
18// Insert a word into the Trie
19function insert(word: string): void {
20 let currentNode: TrieNode = trieRoot;
21
22 // Traverse through each character in the word
23 for (const character of word) {
24 // Calculate array index for the character (a=0, b=1, ..., z=25)
25 const charIndex: number = character.charCodeAt(0) - 97;
26
27 // Create new node if path doesn't exist
28 if (!currentNode.children[charIndex]) {
29 currentNode.children[charIndex] = {
30 children: new Array(26).fill(null),
31 isEndOfWord: false
32 };
33 }
34
35 // Move to the next node
36 currentNode = currentNode.children[charIndex]!;
37 }
38
39 // Mark the end of the word
40 currentNode.isEndOfWord = true;
41}
42
43// Search for a complete word in the Trie
44function search(word: string): boolean {
45 const nodeFound: TrieNode | null = searchPrefix(word);
46 // Return true only if the word exists and is marked as complete
47 return nodeFound !== null && nodeFound.isEndOfWord;
48}
49
50// Check if any word in the Trie starts with the given prefix
51function startsWith(prefix: string): boolean {
52 // Return true if the prefix exists in the Trie
53 return searchPrefix(prefix) !== null;
54}
55
56// Helper function to search for a prefix in the Trie
57function searchPrefix(prefix: string): TrieNode | null {
58 let currentNode: TrieNode = trieRoot;
59
60 // Traverse through each character in the prefix
61 for (const character of prefix) {
62 // Calculate array index for the character (a=0, b=1, ..., z=25)
63 const charIndex: number = character.charCodeAt(0) - 97;
64
65 // Return null if path doesn't exist
66 if (!currentNode.children[charIndex]) {
67 return null;
68 }
69
70 // Move to the next node
71 currentNode = currentNode.children[charIndex]!;
72 }
73
74 // Return the node where the prefix ends
75 return currentNode;
76}
77
Time and Space Complexity
Time Complexity
Insert Operation: O(m)
where m
is the length of the word being inserted.
- We traverse through each character of the word exactly once
- For each character, we perform constant time operations: calculating the index (
ord(c) - ord('a')
), checking if a child exists, creating a new node if needed, and moving to the next node - Setting
is_end = True
at the end isO(1)
Search Operation: O(m)
where m
is the length of the word being searched.
- Calls
_search_prefix
which takesO(m)
time - Additionally checks the
is_end
flag which isO(1)
- Overall complexity remains
O(m)
StartsWith Operation: O(p)
where p
is the length of the prefix.
- Directly calls
_search_prefix
which traverses through each character of the prefix - Each character lookup is
O(1)
in the children array
_search_prefix Helper: O(p)
where p
is the length of the prefix.
- Iterates through each character in the prefix
- For each character, performs constant time array access and index calculation
Space Complexity
Per Node Space: Each Trie node contains:
- An array of 26 pointers (for lowercase English letters a-z):
O(26)
=O(1)
- A boolean flag
is_end
:O(1)
Overall Space Complexity: O(n Γ 26)
= O(n)
where n
is the total number of nodes in the Trie.
- In the worst case, if we insert
k
words with average lengthm
and no common prefixes, we would havek Γ m
nodes - In the best case with maximum prefix sharing, we could have as few as
m
nodes (all words share the same path) - The space complexity is proportional to the total number of unique prefixes across all inserted words
Space per Operation:
- Insert:
O(m)
in worst case when creatingm
new nodes for a word of lengthm
- Search and StartsWith:
O(1)
additional space (only using variables for traversal)
Common Pitfalls
1. Confusing Node Creation Logic
A frequent mistake is creating unnecessary Trie nodes or incorrectly checking for existing nodes during insertion.
Incorrect Implementation:
def insert(self, word: str) -> None:
node = self
for c in word:
idx = ord(c) - ord('a')
node.children[idx] = Trie() # Always creates new node, overwriting existing ones!
node = node.children[idx]
node.is_end = True
Why it's wrong: This overwrites existing nodes, destroying previously inserted words that share prefixes.
Correct Approach:
def insert(self, word: str) -> None:
node = self
for c in word:
idx = ord(c) - ord('a')
if node.children[idx] is None: # Only create if doesn't exist
node.children[idx] = Trie()
node = node.children[idx]
node.is_end = True
2. Forgetting the is_end
Flag in Search
Many implementations forget to check if a node marks the end of a word, causing incorrect search results.
Incorrect Implementation:
def search(self, word: str) -> bool:
node = self._search_prefix(word)
return node is not None # Missing is_end check!
Why it's wrong: This would return True
for "app" even if only "apple" was inserted, since "app" exists as a prefix.
Correct Approach:
def search(self, word: str) -> bool:
node = self._search_prefix(word)
return node is not None and node.is_end # Must check both conditions
3. Character Index Calculation Errors
Incorrect index calculation can lead to array out-of-bounds errors or wrong character mappings.
Common Mistakes:
# Wrong: Using character directly as index
idx = ord(c) # Could be 97-122, exceeding array bounds!
# Wrong: Subtracting wrong base
idx = ord(c) - ord('A') # Wrong for lowercase letters
# Wrong: Not handling the case properly
idx = c - 'a' # Python doesn't support direct char arithmetic
Correct Approach:
idx = ord(c) - ord('a') # Converts 'a'->0, 'b'->1, ..., 'z'->25
4. Memory Inefficiency with Full Node Initialization
Creating all 26 children immediately for every node wastes memory.
Inefficient:
def __init__(self):
self.children = [Trie() for _ in range(26)] # Creates 26 nodes immediately!
self.is_end = False
Efficient:
def __init__(self):
self.children = [None] * 26 # Only creates array, nodes created on demand
self.is_end = False
5. Not Handling Edge Cases
Failing to consider empty strings or special cases.
Potential Issues:
- Inserting empty string ""
- Searching for empty string
- Inserting the same word multiple times
Robust Solution:
def insert(self, word: str) -> None:
if not word: # Handle empty string
self.is_end = True # Mark root as end if needed
return
node = self
for c in word:
idx = ord(c) - ord('a')
if node.children[idx] is None:
node.children[idx] = Trie()
node = node.children[idx]
node.is_end = True
6. Incorrect Variable Naming Leading to Confusion
Using inconsistent naming between is_end
and is_end_of_word
can cause attribute errors.
Error-Prone:
# In __init__: self.is_end_of_word = False # But in insert: node.is_end = True # AttributeError!
Solution: Use consistent naming throughout the implementation.
How does quick sort divide the problem into subproblems?
Recommended Readings
Trie Introduction Imagine you have a magic dictionary where as soon as you start spelling a word it immediately tells you how many words that start with those letters How cool would that be This is exactly the kind of magic a data structure called trie performs What's a Trie
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!