Facebook Pixel

208. Implement Trie (Prefix Tree)

MediumDesignTrieHash TableString
Leetcode Link

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:

  1. Trie(): Constructor that initializes an empty trie.

  2. insert(word): Takes a string word and adds it to the trie. After insertion, the word should be stored in the trie structure.

  3. search(word): Takes a string word and checks if the exact word exists in the trie. Returns true if the word was previously inserted, false otherwise.

  4. startsWith(prefix): Takes a string prefix and checks if any word in the trie starts with this prefix. Returns true 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") returns true
  • search("app") returns false (since "app" was never inserted as a complete word)
  • startsWith("app") returns true (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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 with None values. Each index corresponds to a letter ('a' at index 0, 'b' at index 1, etc.)
  • self.is_end: A boolean flag set to False 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:

  1. Calculate the index for each character using ord(c) - ord('a') (converts 'a' to 0, 'b' to 1, etc.)
  2. If no child exists at that index, create a new Trie node
  3. Move to the child node and continue with the next character
  4. 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:

  1. For each character, calculate its index and check if a child exists
  2. If any character doesn't have a corresponding child, return None (prefix doesn't exist)
  3. 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:

  1. Use _search_prefix to find the node representing the last character
  2. Return True only if the node exists AND is_end is True (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:

  1. Use _search_prefix to traverse the prefix path
  2. Return True if we can successfully traverse all characters (node is not None)
  3. 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 Evaluator

Example Walkthrough

Let's walk through a concrete example to understand how the Trie works:

Example Operations:

  1. Insert "cat"
  2. Insert "car"
  3. Search "cat" β†’ returns True
  4. Search "ca" β†’ returns False
  5. 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 is O(1)

Search Operation: O(m) where m is the length of the word being searched.

  • Calls _search_prefix which takes O(m) time
  • Additionally checks the is_end flag which is O(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 length m and no common prefixes, we would have k Γ— 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 creating m new nodes for a word of length m
  • 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does quick sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More