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 oftimes
(both of lengthn
) sentences[i]
represents a previously typed sentencetimes[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:
- Sentences are ranked by their "hot degree" (frequency of being typed)
- Higher frequency sentences appear first
- If two sentences have the same frequency, they're sorted in ASCII order (lexicographically)
- 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:
AutocompleteSystem(sentences, times)
: Constructor that initializes the system with historical datainput(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
- If
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:
- After finding the node representing the current prefix, we need to explore all possible completions below that node
- We must visit every descendant node that represents a complete sentence (where
node.v > 0
) - DFS naturally explores the entire subtree rooted at the prefix node, collecting all valid sentences
The algorithm flow is:
- Build a Trie from historical sentences
- As user types, navigate to the node representing the current prefix
- Use DFS to explore all descendants and collect sentences
- Sort collected sentences by frequency and lexicographic order
- Return top 3 results
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:
- Build phase: Insert all historical sentences into the Trie, storing frequency at leaf nodes
- Search phase: Navigate to the prefix node as user types
- Collection phase: Use DFS to gather all sentences in the subtree
- 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 nodew
: 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 sentencesself.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:
-
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
-
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 EvaluatorExample 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"
- Current buffer:
["i"]
- Search for prefix "i" in Trie β finds node at 'i'
- DFS from this node collects all sentences in subtree:
- Found: ("i love you", 5)
- Found: ("island", 3)
- Found: ("i love coding", 2)
- Sort by frequency (desc), then alphabetically:
- ["i love you" (5), "island" (3), "i love coding" (2)]
- Return top 3:
["i love you", "island", "i love coding"]
Step 3: User Types " " (space)
- Current buffer:
["i", " "]
- Search for prefix "i " β finds node at 'i' β ' '
- DFS collects sentences starting with "i ":
- Found: ("i love you", 5)
- Found: ("i love coding", 2)
- Sort results:
- ["i love you" (5), "i love coding" (2)]
- Return:
["i love you", "i love coding"]
Step 4: User Types "a"
- Current buffer:
["i", " ", "a"]
- Search for prefix "i a" β returns None (no such prefix exists)
- Return:
[]
Step 5: User Types "#"
- Current sentence: "i a"
- Insert "i a" into Trie with frequency 1
- Trie now has a new path: root β i β ' ' β a (v=1, w="i a")
- Clear buffer:
[]
- Return:
[]
Step 6: User Types "i" Again
- Current buffer:
["i"]
- Search for prefix "i" β finds node
- DFS collects:
- ("i love you", 5)
- ("island", 3)
- ("i love coding", 2)
- ("i a", 1) β newly added
- 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)
wheren
is the number of sentences andm
is the average length of each sentence. Each sentence needs to be inserted into the Trie, and insertion takesO(m)
time per sentence. -
input
method:- When
c == '#'
:O(p)
wherep
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)
wherep
is the current length of the typed prefix - DFS traversal:
O(N)
whereN
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)
wherek
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 isO(p + N)
- Searching for the prefix node:
- When
Space Complexity:
-
Trie structure:
O(T * 27)
whereT
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)
wherep
is the maximum length of the current input sequence - The
res
list in DFS:O(k)
wherek
is the number of complete sentences found
- The Trie itself:
-
Overall space complexity:
O(T * 27 + p + k)
, which simplifies toO(T)
as the Trie dominates the space usage. -
DFS recursion stack:
O(h)
whereh
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)
How does quick sort divide the problem into subproblems?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
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
Median of Data Stream Given a stream of numbers find the median number at any given time accurate to 1 decimal place Example add_number 1 add_number 2 add_number 3 get_median 2 0 add_number 4 get_median 2 5 Try it yourself Explanation Intuition Brute force way is to sort the entire list every time we get a new number This would be O N log N for each add_number operation One step up is to notice that the list is sorted before we add any new number to it So every
Want a Structured Path to Master System Design Too? Donβt Miss This!