Facebook Pixel

642. Design Search Autocomplete System πŸ”’

Problem Description

You need to design an autocomplete system for a search engine that suggests sentences as users type. The system works with the following specifications:

Initial Setup:

  • You're given an array of sentences and an array of times (both of length n)
  • sentences[i] represents a previously typed sentence
  • times[i] represents how many times that sentence was typed

How the System Works:

  • Users input characters one by one to form a sentence
  • Each sentence ends with the special character '#'
  • For each character typed (except '#'), the system returns the top 3 most frequent sentences that start with the prefix typed so far

Ranking Rules:

  1. Sentences are ranked by their "hot degree" (frequency of being typed)
  2. Higher frequency sentences appear first
  3. If two sentences have the same frequency, they're sorted in ASCII order (lexicographically)
  4. If fewer than 3 matching sentences exist, return all available matches

Special Behavior:

  • When '#' is input, it means the current sentence is complete
  • The completed sentence is added to the system (increasing its count by 1 if it already exists)
  • After '#' is input, return an empty list and reset for the next sentence

Implementation Requirements:

The AutocompleteSystem class needs two methods:

  1. AutocompleteSystem(sentences, times): Constructor that initializes the system with historical data
  2. input(c): Processes a single character input
    • If c == '#': saves the current sentence and returns []
    • Otherwise: returns a list of up to 3 sentence suggestions based on the current prefix

Example Flow: If a user types "i" then " " then "a", the system would:

  • After "i": return top 3 sentences starting with "i"
  • After "i ": return top 3 sentences starting with "i "
  • After "i a": return top 3 sentences starting with "i a"
  • After "i a#": save "i a" to the system and return []

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The problem involves a Trie data structure, which is essentially a tree (a special type of graph). Each node in the Trie represents a character, and edges connect characters to form words/sentences. The autocomplete system needs to traverse this tree structure to find matching prefixes.

Is it a tree?

  • Yes: A Trie is specifically a tree data structure where each path from root to a leaf (or intermediate node) represents a word or sentence. There are no cycles, and each node (except the root) has exactly one parent.

DFS

  • Yes: The solution uses DFS to traverse the Trie and collect all sentences with a given prefix.

Conclusion: The flowchart correctly leads us to DFS as the pattern for this problem.

Why DFS is Used Here

Looking at the solution code, we can see DFS is implemented in the input method:

def dfs(node):
    if node is None:
        return
    if node.v:  # If this node represents a complete sentence
        res.append((node.v, node.w))
    for nxt in node.children:  # Recursively explore all children
        dfs(nxt)

The DFS traversal is necessary because:

  1. After finding the node representing the current prefix, we need to explore all possible completions below that node
  2. We must visit every descendant node that represents a complete sentence (where node.v > 0)
  3. DFS naturally explores the entire subtree rooted at the prefix node, collecting all valid sentences

The algorithm flow is:

  1. Build a Trie from historical sentences
  2. As user types, navigate to the node representing the current prefix
  3. Use DFS to explore all descendants and collect sentences
  4. Sort collected sentences by frequency and lexicographic order
  5. Return top 3 results
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that we need a data structure that can efficiently store sentences and retrieve all sentences with a common prefix. When someone types "i sp", we need to quickly find all sentences starting with "i sp" like "i speak", "i spent time", etc.

A Trie (prefix tree) is perfect for this because:

  • Each path from root represents a prefix
  • We can navigate character by character as the user types
  • All sentences with the same prefix share the same path initially

Think of it like a dictionary where instead of looking up complete words, you're finding all words that start with certain letters. As you type "c-a-t", you first go to all words starting with "c", then narrow down to "ca", then to "cat".

The challenge is that after finding the node representing our current prefix, we need to discover ALL possible sentence completions below that node. This is where DFS comes in - we need to explore every branch of the subtree to collect all valid sentences.

Why DFS over BFS? Because we need to collect complete sentences (which are at various depths), not process level by level. DFS naturally follows each path to completion, making it easier to track and store complete sentences.

The workflow becomes:

  1. Build phase: Insert all historical sentences into the Trie, storing frequency at leaf nodes
  2. Search phase: Navigate to the prefix node as user types
  3. Collection phase: Use DFS to gather all sentences in the subtree
  4. Ranking phase: Sort by frequency and alphabetically, return top 3

The special character '#' acts as a commit - it adds the typed sentence to our Trie (incrementing its count), making the system learn from user input over time. This creates a feedback loop where frequently typed sentences become higher-ranked suggestions.

Learn more about Depth-First Search, Trie, Data Stream, Sorting and Heap (Priority Queue) patterns.

Solution Approach

The solution implements a Trie-based autocomplete system with DFS traversal for collecting suggestions. Let's break down the implementation:

Trie Data Structure

The [Trie](/problems/trie_intro) class is designed to store sentences efficiently:

  • children: An array of 27 elements (26 for letters 'a'-'z' and 1 for space character at index 26)
  • v: Stores the frequency count of the sentence ending at this node
  • w: Stores the complete sentence (only at nodes where sentences end)
def insert(self, w, t):
    node = self
    for c in w:
        idx = 26 if c == ' ' else ord(c) - ord('a')
        if node.children[idx] is None:
            node.children[idx] = [Trie](/problems/trie_intro)()
        node = node.children[idx]
    node.v += t  # Increment frequency
    node.w = w   # Store complete sentence

The insert method navigates through the Trie, creating nodes as needed, and updates the frequency at the sentence's end node.

Search for Prefix Node

The search method finds the node representing the typed prefix:

def search(self, pref):
    node = self
    for c in pref:
        idx = 26 if c == ' ' else ord(c) - ord('a')
        if node.children[idx] is None:
            return None  # Prefix doesn't exist
        node = node.children[idx]
    return node

AutocompleteSystem Implementation

The main class maintains:

  • self.[trie](/problems/trie_intro): The Trie storing all sentences
  • self.t: A list tracking the current input being typed

Initialization: Populates the Trie with historical data:

for a, b in zip(sentences, times):
    self.trie.insert(a, b)

Input Processing: The input method handles two cases:

  1. When c == '#':

    • Joins the accumulated characters into a sentence
    • Inserts it into the Trie with count 1
    • Resets the current input buffer
    • Returns empty list
  2. For regular characters:

    • Appends character to current input buffer
    • Searches for the prefix node
    • Uses DFS to collect all sentences below that node
    • Sorts results by frequency (descending) then lexicographically
    • Returns top 3

DFS Collection

The nested dfs function recursively explores the subtree:

def dfs(node):
    if node is None:
        return
    if node.v:  # Found a complete sentence
        res.append((node.v, node.w))
    for nxt in node.children:
        dfs(nxt)  # Explore all children

This collects all sentences as (frequency, sentence) tuples.

Sorting and Ranking

Results are sorted using a custom key:

res.sort(key=lambda x: (-x[0], x[1]))
  • -x[0]: Negative frequency for descending order (higher frequency first)
  • x[1]: Sentence string for ascending alphabetical order when frequencies are equal

Finally, the top 3 sentences are extracted and returned:

return [v[1] for v in res[:3]]

The time complexity for each input operation is O(p + m + mlogm) where p is the prefix length, m is the number of sentences with that prefix, and the space complexity is O(total characters in all sentences) for storing the Trie.

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 small example to illustrate how the autocomplete system works.

Initial Setup:

sentences = ["i love you", "island", "i love coding"]
times = [5, 3, 2]

Step 1: Building the Trie

The system builds a Trie where each path represents a sentence:

root
 └─ i (no value)
     β”œβ”€ ' ' (space)
     β”‚   └─ l
     β”‚       └─ o
     β”‚           └─ v
     β”‚               └─ e
     β”‚                   β”œβ”€ ' '
     β”‚                   β”‚   β”œβ”€ y
     β”‚                   β”‚   β”‚   └─ o
     β”‚                   β”‚   β”‚       └─ u (v=5, w="i love you")
     β”‚                   β”‚   └─ c
     β”‚                   β”‚       └─ o
     β”‚                   β”‚           └─ d
     β”‚                   β”‚               └─ i
     β”‚                   β”‚                   └─ n
     β”‚                   β”‚                       └─ g (v=2, w="i love coding")
     └─ s
         └─ l
             └─ a
                 └─ n
                     └─ d (v=3, w="island")

Step 2: User Types "i"

  1. Current buffer: ["i"]
  2. Search for prefix "i" in Trie β†’ finds node at 'i'
  3. DFS from this node collects all sentences in subtree:
    • Found: ("i love you", 5)
    • Found: ("island", 3)
    • Found: ("i love coding", 2)
  4. Sort by frequency (desc), then alphabetically:
    • ["i love you" (5), "island" (3), "i love coding" (2)]
  5. Return top 3: ["i love you", "island", "i love coding"]

Step 3: User Types " " (space)

  1. Current buffer: ["i", " "]
  2. Search for prefix "i " β†’ finds node at 'i' β†’ ' '
  3. DFS collects sentences starting with "i ":
    • Found: ("i love you", 5)
    • Found: ("i love coding", 2)
  4. Sort results:
    • ["i love you" (5), "i love coding" (2)]
  5. Return: ["i love you", "i love coding"]

Step 4: User Types "a"

  1. Current buffer: ["i", " ", "a"]
  2. Search for prefix "i a" β†’ returns None (no such prefix exists)
  3. Return: []

Step 5: User Types "#"

  1. Current sentence: "i a"
  2. Insert "i a" into Trie with frequency 1
  3. Trie now has a new path: root β†’ i β†’ ' ' β†’ a (v=1, w="i a")
  4. Clear buffer: []
  5. Return: []

Step 6: User Types "i" Again

  1. Current buffer: ["i"]
  2. Search for prefix "i" β†’ finds node
  3. DFS collects:
    • ("i love you", 5)
    • ("island", 3)
    • ("i love coding", 2)
    • ("i a", 1) ← newly added
  4. Sort and return top 3: ["i love you", "island", "i love coding"]

This example demonstrates:

  • How the Trie efficiently stores sentences with common prefixes
  • How DFS collects all matching sentences from a subtree
  • How the ranking system prioritizes by frequency, then alphabetically
  • How the system learns by adding new sentences when '#' is typed

Solution Implementation

1from typing import List, Optional
2
3class TrieNode:
4    def __init__(self):
5        # 26 letters + 1 space character
6        self.children = [None] * 27
7        # Frequency count of the sentence ending at this node
8        self.frequency = 0
9        # The complete sentence if this node represents end of a sentence
10        self.sentence = ''
11
12    def insert(self, sentence: str, count: int) -> None:
13        """Insert a sentence into the trie with its frequency count."""
14        current_node = self
15      
16        for char in sentence:
17            # Calculate index: 26 for space, 0-25 for 'a'-'z'
18            index = 26 if char == ' ' else ord(char) - ord('a')
19          
20            # Create new node if path doesn't exist
21            if current_node.children[index] is None:
22                current_node.children[index] = TrieNode()
23          
24            current_node = current_node.children[index]
25      
26        # Update frequency and store the complete sentence at leaf node
27        current_node.frequency += count
28        current_node.sentence = sentence
29
30    def search(self, prefix: str) -> Optional['TrieNode']:
31        """Search for a prefix in the trie and return the ending node."""
32        current_node = self
33      
34        for char in prefix:
35            # Calculate index: 26 for space, 0-25 for 'a'-'z'
36            index = 26 if char == ' ' else ord(char) - ord('a')
37          
38            # Prefix doesn't exist in trie
39            if current_node.children[index] is None:
40                return None
41          
42            current_node = current_node.children[index]
43      
44        return current_node
45
46
47class AutocompleteSystem:
48    def __init__(self, sentences: List[str], times: List[int]):
49        """Initialize the autocomplete system with historical data."""
50        # Initialize the trie root
51        self.trie_root = TrieNode()
52      
53        # Insert all historical sentences with their frequencies
54        for sentence, frequency in zip(sentences, times):
55            self.trie_root.insert(sentence, frequency)
56      
57        # Buffer to store current input characters
58        self.current_input = []
59
60    def input(self, c: str) -> List[str]:
61        """
62        Process input character and return top 3 autocomplete suggestions.
63      
64        Args:
65            c: Input character (lowercase letter, space, or '#')
66          
67        Returns:
68            List of top 3 sentences sorted by frequency (desc) and lexicographically
69        """
70      
71        def collect_all_sentences(node: Optional[TrieNode]) -> None:
72            """DFS to collect all sentences from given node and its subtree."""
73            if node is None:
74                return
75          
76            # If this node represents end of a sentence, add to results
77            if node.frequency > 0:
78                autocomplete_results.append((node.frequency, node.sentence))
79          
80            # Recursively search all children
81            for child_node in node.children:
82                collect_all_sentences(child_node)
83      
84        # Handle end of input - save the sentence
85        if c == '#':
86            # Construct the complete sentence from buffer
87            completed_sentence = ''.join(self.current_input)
88            # Add this new sentence to trie with frequency 1
89            self.trie_root.insert(completed_sentence, 1)
90            # Clear the buffer for next input
91            self.current_input = []
92            return []
93      
94        # Add current character to buffer
95        self.current_input.append(c)
96      
97        # Search for current prefix in trie
98        current_prefix = ''.join(self.current_input)
99        prefix_node = self.trie_root.search(current_prefix)
100      
101        # If prefix doesn't exist, return empty list
102        if prefix_node is None:
103            return []
104      
105        # Collect all sentences starting with current prefix
106        autocomplete_results = []
107        collect_all_sentences(prefix_node)
108      
109        # Sort by frequency (descending) and then lexicographically
110        autocomplete_results.sort(key=lambda x: (-x[0], x[1]))
111      
112        # Return top 3 sentences (extract just the sentence strings)
113        return [sentence for frequency, sentence in autocomplete_results[:3]]
114
115
116# Your AutocompleteSystem object will be instantiated and called as such:
117# obj = AutocompleteSystem(sentences, times)
118# param_1 = obj.input(c)
119
1/**
2 * Trie node implementation for storing sentences with their frequencies
3 */
4class Trie {
5    // Array to store child nodes (26 letters + 1 space character)
6    Trie[] children = new Trie[27];
7    // Frequency count of the sentence ending at this node
8    int frequency;
9    // The complete sentence stored at this node
10    String sentence = "";
11
12    /**
13     * Inserts a sentence into the trie with given frequency
14     * @param sentence the sentence to insert
15     * @param times the frequency/times to add for this sentence
16     */
17    void insert(String sentence, int times) {
18        Trie currentNode = this;
19      
20        // Traverse through each character in the sentence
21        for (char ch : sentence.toCharArray()) {
22            // Map character to index: space -> 26, 'a'-'z' -> 0-25
23            int index = ch == ' ' ? 26 : ch - 'a';
24          
25            // Create new node if path doesn't exist
26            if (currentNode.children[index] == null) {
27                currentNode.children[index] = new Trie();
28            }
29            currentNode = currentNode.children[index];
30        }
31      
32        // Update frequency and store sentence at leaf node
33        currentNode.frequency += times;
34        currentNode.sentence = sentence;
35    }
36
37    /**
38     * Searches for a node with given prefix
39     * @param prefix the prefix to search for
40     * @return the trie node at the end of prefix path, or null if not found
41     */
42    Trie search(String prefix) {
43        Trie currentNode = this;
44      
45        // Traverse the trie following the prefix path
46        for (char ch : prefix.toCharArray()) {
47            // Map character to index: space -> 26, 'a'-'z' -> 0-25
48            int index = ch == ' ' ? 26 : ch - 'a';
49          
50            // Return null if prefix path doesn't exist
51            if (currentNode.children[index] == null) {
52                return null;
53            }
54            currentNode = currentNode.children[index];
55        }
56      
57        return currentNode;
58    }
59}
60
61/**
62 * Autocomplete system that suggests top 3 sentences based on user input
63 */
64class AutocompleteSystem {
65    // Root of the trie storing all sentences
66    private Trie trieRoot = new Trie();
67    // Buffer to store current user input
68    private StringBuilder currentInput = new StringBuilder();
69
70    /**
71     * Constructor to initialize the autocomplete system
72     * @param sentences array of initial sentences
73     * @param times array of frequencies corresponding to each sentence
74     */
75    public AutocompleteSystem(String[] sentences, int[] times) {
76        // Insert all initial sentences with their frequencies into the trie
77        int index = 0;
78        for (String sentence : sentences) {
79            trieRoot.insert(sentence, times[index++]);
80        }
81    }
82
83    /**
84     * Processes user input character and returns autocomplete suggestions
85     * @param c the input character
86     * @return list of top 3 autocomplete suggestions, or empty list if c is '#'
87     */
88    public List<String> input(char c) {
89        List<String> suggestions = new ArrayList<>();
90      
91        // Handle end of sentence input
92        if (c == '#') {
93            // Add the completed sentence to the trie with frequency 1
94            trieRoot.insert(currentInput.toString(), 1);
95            // Reset the input buffer for next sentence
96            currentInput = new StringBuilder();
97            return suggestions;
98        }
99      
100        // Append character to current input
101        currentInput.append(c);
102      
103        // Search for the prefix in the trie
104        Trie prefixNode = trieRoot.search(currentInput.toString());
105        if (prefixNode == null) {
106            return suggestions;
107        }
108      
109        // Use min heap to maintain top 3 sentences
110        // Priority: lower frequency first, then reverse alphabetical order
111        PriorityQueue<Trie> minHeap = new PriorityQueue<>((a, b) -> 
112            a.frequency == b.frequency ? b.sentence.compareTo(a.sentence) : a.frequency - b.frequency);
113      
114        // Find all sentences with the current prefix
115        dfs(prefixNode, minHeap);
116      
117        // Extract results from heap in reverse order
118        while (!minHeap.isEmpty()) {
119            suggestions.add(0, minHeap.poll().sentence);
120        }
121      
122        return suggestions;
123    }
124
125    /**
126     * Depth-first search to find all sentences in the subtree
127     * Maintains only top 3 sentences in the priority queue
128     * @param node current trie node
129     * @param minHeap priority queue to store top 3 sentences
130     */
131    private void dfs(Trie node, PriorityQueue<Trie> minHeap) {
132        if (node == null) {
133            return;
134        }
135      
136        // If this node represents a complete sentence
137        if (node.frequency > 0) {
138            minHeap.offer(node);
139            // Keep only top 3 sentences
140            if (minHeap.size() > 3) {
141                minHeap.poll();
142            }
143        }
144      
145        // Recursively search all children
146        for (Trie child : node.children) {
147            dfs(child, minHeap);
148        }
149    }
150}
151
152/**
153 * Your AutocompleteSystem object will be instantiated and called as such:
154 * AutocompleteSystem obj = new AutocompleteSystem(sentences, times);
155 * List<String> param_1 = obj.input(c);
156 */
157
1#include <vector>
2#include <string>
3#include <queue>
4#include <algorithm>
5
6using namespace std;
7
8/**
9 * Trie node implementation for storing sentences with their frequencies
10 */
11class TrieNode {
12public:
13    // Array to store child nodes (26 letters + 1 space character)
14    TrieNode* children[27];
15    // Frequency count of the sentence ending at this node
16    int frequency;
17    // The complete sentence stored at this node
18    string sentence;
19  
20    /**
21     * Constructor to initialize the trie node
22     */
23    TrieNode() {
24        for (int i = 0; i < 27; i++) {
25            children[i] = nullptr;
26        }
27        frequency = 0;
28        sentence = "";
29    }
30  
31    /**
32     * Inserts a sentence into the trie with given frequency
33     * @param sentence the sentence to insert
34     * @param times the frequency/times to add for this sentence
35     */
36    void insert(const string& sentence, int times) {
37        TrieNode* currentNode = this;
38      
39        // Traverse through each character in the sentence
40        for (char ch : sentence) {
41            // Map character to index: space -> 26, 'a'-'z' -> 0-25
42            int index = (ch == ' ') ? 26 : (ch - 'a');
43          
44            // Create new node if path doesn't exist
45            if (currentNode->children[index] == nullptr) {
46                currentNode->children[index] = new TrieNode();
47            }
48            currentNode = currentNode->children[index];
49        }
50      
51        // Update frequency and store sentence at leaf node
52        currentNode->frequency += times;
53        currentNode->sentence = sentence;
54    }
55  
56    /**
57     * Searches for a node with given prefix
58     * @param prefix the prefix to search for
59     * @return the trie node at the end of prefix path, or nullptr if not found
60     */
61    TrieNode* search(const string& prefix) {
62        TrieNode* currentNode = this;
63      
64        // Traverse the trie following the prefix path
65        for (char ch : prefix) {
66            // Map character to index: space -> 26, 'a'-'z' -> 0-25
67            int index = (ch == ' ') ? 26 : (ch - 'a');
68          
69            // Return nullptr if prefix path doesn't exist
70            if (currentNode->children[index] == nullptr) {
71                return nullptr;
72            }
73            currentNode = currentNode->children[index];
74        }
75      
76        return currentNode;
77    }
78};
79
80/**
81 * Autocomplete system that suggests top 3 sentences based on user input
82 */
83class AutocompleteSystem {
84private:
85    // Root of the trie storing all sentences
86    TrieNode* trieRoot;
87    // Buffer to store current user input
88    string currentInput;
89  
90    /**
91     * Comparator for the priority queue
92     * Priority: lower frequency first, then reverse alphabetical order
93     */
94    struct TrieNodeComparator {
95        bool operator()(TrieNode* a, TrieNode* b) {
96            if (a->frequency == b->frequency) {
97                return a->sentence < b->sentence;  // Reverse alphabetical for min heap
98            }
99            return a->frequency > b->frequency;  // Lower frequency has higher priority for min heap
100        }
101    };
102  
103    /**
104     * Depth-first search to find all sentences in the subtree
105     * Maintains only top 3 sentences in the priority queue
106     * @param node current trie node
107     * @param minHeap priority queue to store top 3 sentences
108     */
109    void dfs(TrieNode* node, priority_queue<TrieNode*, vector<TrieNode*>, TrieNodeComparator>& minHeap) {
110        if (node == nullptr) {
111            return;
112        }
113      
114        // If this node represents a complete sentence
115        if (node->frequency > 0) {
116            minHeap.push(node);
117            // Keep only top 3 sentences
118            if (minHeap.size() > 3) {
119                minHeap.pop();
120            }
121        }
122      
123        // Recursively search all children
124        for (int i = 0; i < 27; i++) {
125            dfs(node->children[i], minHeap);
126        }
127    }
128  
129public:
130    /**
131     * Constructor to initialize the autocomplete system
132     * @param sentences array of initial sentences
133     * @param times array of frequencies corresponding to each sentence
134     */
135    AutocompleteSystem(vector<string>& sentences, vector<int>& times) {
136        trieRoot = new TrieNode();
137        currentInput = "";
138      
139        // Insert all initial sentences with their frequencies into the trie
140        for (int i = 0; i < sentences.size(); i++) {
141            trieRoot->insert(sentences[i], times[i]);
142        }
143    }
144  
145    /**
146     * Processes user input character and returns autocomplete suggestions
147     * @param c the input character
148     * @return list of top 3 autocomplete suggestions, or empty list if c is '#'
149     */
150    vector<string> input(char c) {
151        vector<string> suggestions;
152      
153        // Handle end of sentence input
154        if (c == '#') {
155            // Add the completed sentence to the trie with frequency 1
156            trieRoot->insert(currentInput, 1);
157            // Reset the input buffer for next sentence
158            currentInput = "";
159            return suggestions;
160        }
161      
162        // Append character to current input
163        currentInput += c;
164      
165        // Search for the prefix in the trie
166        TrieNode* prefixNode = trieRoot->search(currentInput);
167        if (prefixNode == nullptr) {
168            return suggestions;
169        }
170      
171        // Use min heap to maintain top 3 sentences
172        // Priority: lower frequency first, then reverse alphabetical order
173        priority_queue<TrieNode*, vector<TrieNode*>, TrieNodeComparator> minHeap;
174      
175        // Find all sentences with the current prefix
176        dfs(prefixNode, minHeap);
177      
178        // Extract results from heap in reverse order
179        while (!minHeap.empty()) {
180            suggestions.insert(suggestions.begin(), minHeap.top()->sentence);
181            minHeap.pop();
182        }
183      
184        return suggestions;
185    }
186};
187
188/**
189 * Your AutocompleteSystem object will be instantiated and called as such:
190 * AutocompleteSystem* obj = new AutocompleteSystem(sentences, times);
191 * vector<string> param_1 = obj->input(c);
192 */
193
1/**
2 * Trie node structure for storing sentences with their frequencies
3 */
4interface TrieNode {
5    // Array to store child nodes (26 letters + 1 space character)
6    children: (TrieNode | null)[];
7    // Frequency count of the sentence ending at this node
8    frequency: number;
9    // The complete sentence stored at this node
10    sentence: string;
11}
12
13/**
14 * Creates a new trie node
15 */
16function createTrieNode(): TrieNode {
17    return {
18        children: new Array(27).fill(null),
19        frequency: 0,
20        sentence: ""
21    };
22}
23
24/**
25 * Inserts a sentence into the trie with given frequency
26 * @param root - The root node of the trie
27 * @param sentence - The sentence to insert
28 * @param times - The frequency/times to add for this sentence
29 */
30function insertIntoTrie(root: TrieNode, sentence: string, times: number): void {
31    let currentNode = root;
32  
33    // Traverse through each character in the sentence
34    for (const char of sentence) {
35        // Map character to index: space -> 26, 'a'-'z' -> 0-25
36        const index = char === ' ' ? 26 : char.charCodeAt(0) - 'a'.charCodeAt(0);
37      
38        // Create new node if path doesn't exist
39        if (currentNode.children[index] === null) {
40            currentNode.children[index] = createTrieNode();
41        }
42        currentNode = currentNode.children[index]!;
43    }
44  
45    // Update frequency and store sentence at leaf node
46    currentNode.frequency += times;
47    currentNode.sentence = sentence;
48}
49
50/**
51 * Searches for a node with given prefix
52 * @param root - The root node of the trie
53 * @param prefix - The prefix to search for
54 * @returns The trie node at the end of prefix path, or null if not found
55 */
56function searchInTrie(root: TrieNode, prefix: string): TrieNode | null {
57    let currentNode = root;
58  
59    // Traverse the trie following the prefix path
60    for (const char of prefix) {
61        // Map character to index: space -> 26, 'a'-'z' -> 0-25
62        const index = char === ' ' ? 26 : char.charCodeAt(0) - 'a'.charCodeAt(0);
63      
64        // Return null if prefix path doesn't exist
65        if (currentNode.children[index] === null) {
66            return null;
67        }
68        currentNode = currentNode.children[index]!;
69    }
70  
71    return currentNode;
72}
73
74// Global variables for the autocomplete system
75let trieRoot: TrieNode;
76let currentInput: string;
77
78/**
79 * Initializes the autocomplete system
80 * @param sentences - Array of initial sentences
81 * @param times - Array of frequencies corresponding to each sentence
82 */
83function AutocompleteSystem(sentences: string[], times: number[]): void {
84    // Initialize the trie root and current input buffer
85    trieRoot = createTrieNode();
86    currentInput = "";
87  
88    // Insert all initial sentences with their frequencies into the trie
89    for (let i = 0; i < sentences.length; i++) {
90        insertIntoTrie(trieRoot, sentences[i], times[i]);
91    }
92}
93
94/**
95 * Depth-first search to find all sentences in the subtree
96 * Maintains only top 3 sentences in the array
97 * @param node - Current trie node
98 * @param topSentences - Array to store top sentences with their frequencies
99 */
100function dfsCollectSentences(node: TrieNode | null, topSentences: TrieNode[]): void {
101    if (node === null) {
102        return;
103    }
104  
105    // If this node represents a complete sentence
106    if (node.frequency > 0) {
107        topSentences.push(node);
108      
109        // Sort to maintain top 3 sentences
110        // Priority: higher frequency first, then alphabetical order
111        topSentences.sort((a, b) => {
112            if (a.frequency === b.frequency) {
113                return a.sentence.localeCompare(b.sentence);
114            }
115            return b.frequency - a.frequency;
116        });
117      
118        // Keep only top 3 sentences
119        if (topSentences.length > 3) {
120            topSentences.pop();
121        }
122    }
123  
124    // Recursively search all children
125    for (const child of node.children) {
126        dfsCollectSentences(child, topSentences);
127    }
128}
129
130/**
131 * Processes user input character and returns autocomplete suggestions
132 * @param c - The input character
133 * @returns List of top 3 autocomplete suggestions, or empty list if c is '#'
134 */
135function input(c: string): string[] {
136    const suggestions: string[] = [];
137  
138    // Handle end of sentence input
139    if (c === '#') {
140        // Add the completed sentence to the trie with frequency 1
141        insertIntoTrie(trieRoot, currentInput, 1);
142        // Reset the input buffer for next sentence
143        currentInput = "";
144        return suggestions;
145    }
146  
147    // Append character to current input
148    currentInput += c;
149  
150    // Search for the prefix in the trie
151    const prefixNode = searchInTrie(trieRoot, currentInput);
152    if (prefixNode === null) {
153        return suggestions;
154    }
155  
156    // Collect all sentences with the current prefix
157    const topSentences: TrieNode[] = [];
158    dfsCollectSentences(prefixNode, topSentences);
159  
160    // Extract sentences from the sorted array
161    for (const node of topSentences) {
162        suggestions.push(node.sentence);
163    }
164  
165    return suggestions;
166}
167

Time and Space Complexity

Time Complexity:

  • Initialization (__init__): O(n * m) where n is the number of sentences and m is the average length of each sentence. Each sentence needs to be inserted into the Trie, and insertion takes O(m) time per sentence.

  • input method:

    • When c == '#': O(p) where p is the length of the current prefix being inserted. The insertion operation traverses through each character of the string.
    • When c != '#':
      • Searching for the prefix node: O(p) where p is the current length of the typed prefix
      • DFS traversal: O(N) where N is the total number of nodes in the subtree rooted at the prefix node (worst case: all nodes in the Trie)
      • Sorting results: O(k * log k) where k is the number of complete sentences found in the subtree
      • Overall: O(p + N + k * log k), but since we only return top 3, the practical complexity is O(p + N)

Space Complexity:

  • Trie structure: O(T * 27) where T is the total number of unique prefixes across all sentences. Each node can have up to 27 children (26 letters + 1 space).

  • AutocompleteSystem class:

    • The Trie itself: O(T * 27)
    • The buffer self.t: O(p) where p is the maximum length of the current input sequence
    • The res list in DFS: O(k) where k is the number of complete sentences found
  • Overall space complexity: O(T * 27 + p + k), which simplifies to O(T) as the Trie dominates the space usage.

  • DFS recursion stack: O(h) where h is the maximum height of the Trie (longest sentence length)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Character Index Mapping

One of the most common mistakes is incorrectly mapping characters to array indices, especially forgetting to handle the space character properly.

Pitfall Example:

# WRONG: This will cause IndexError or incorrect behavior
index = ord(char) - ord('a')  # Doesn't handle space character

Solution: Always check for space character explicitly:

index = 26 if char == ' ' else ord(char) - ord('a')

2. Not Accumulating Frequency for Duplicate Sentences

A critical error is overwriting the frequency instead of accumulating it when the same sentence is inserted multiple times.

Pitfall Example:

# WRONG: This overwrites existing frequency
current_node.frequency = count  # Should be +=

Solution: Always use addition assignment to accumulate frequencies:

current_node.frequency += count

3. Forgetting to Reset Input Buffer After '#'

Failing to clear the input buffer after processing '#' will cause subsequent inputs to be concatenated with the previous sentence.

Pitfall Example:

if c == '#':
    completed_sentence = ''.join(self.current_input)
    self.trie_root.insert(completed_sentence, 1)
    # WRONG: Forgot to reset self.current_input
    return []

Solution: Always reset the buffer after processing a complete sentence:

if c == '#':
    completed_sentence = ''.join(self.current_input)
    self.trie_root.insert(completed_sentence, 1)
    self.current_input = []  # Reset for next input
    return []

4. Incorrect Sorting Logic

The sorting requirements are specific: first by frequency (descending), then lexicographically (ascending). Getting this wrong will produce incorrect top-3 results.

Pitfall Example:

# WRONG: Both in descending order
autocomplete_results.sort(key=lambda x: (-x[0], -x[1]))

# WRONG: Wrong order of sorting criteria
autocomplete_results.sort(key=lambda x: (x[1], -x[0]))

Solution: Use negative frequency for descending order, keep sentence as-is for ascending:

autocomplete_results.sort(key=lambda x: (-x[0], x[1]))

5. Storing Sentence at Every Node Instead of Leaf Nodes

Storing the sentence string at every node along the path wastes memory and can cause confusion.

Pitfall Example:

def insert(self, sentence: str, count: int) -> None:
    current_node = self
    for char in sentence:
        index = 26 if char == ' ' else ord(char) - ord('a')
        if current_node.children[index] is None:
            current_node.children[index] = TrieNode()
        current_node = current_node.children[index]
        current_node.sentence = sentence  # WRONG: Storing at every node

Solution: Only store the complete sentence at the node where it ends:

def insert(self, sentence: str, count: int) -> None:
    current_node = self
    for char in sentence:
        # ... traverse to end node ...
    # Only store at the final node
    current_node.frequency += count
    current_node.sentence = sentence

6. Not Handling Empty Prefix Correctly

When no sentences match the current prefix, the system should return an empty list rather than throwing an error.

Pitfall Example:

prefix_node = self.trie_root.search(current_prefix)
# WRONG: Directly trying to traverse without null check
collect_all_sentences(prefix_node)

Solution: Check if the prefix exists before collecting sentences:

prefix_node = self.trie_root.search(current_prefix)
if prefix_node is None:
    return []
collect_all_sentences(prefix_node)
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