Search Suggestions System
Problem
Given an array of strings products and a string searchWord, return a list of lists of strings representing the suggested products after each character of searchWord is typed, where each suggestion list contains at most three lexicographically minimum products starting with the current prefix.
- 1 ≤ products.length ≤ 1000
- 1 ≤ products[i].length ≤ 3000
- 1 ≤ searchWord.length ≤ 1000
- All strings consist of lowercase English letters.
- All products are unique.
Example
products = ["mobile","mouse","moneypot","monitor","mousepad"], searchWord = "mouse"[["mobile","moneypot","monitor"],["mobile","moneypot","monitor"],["mouse","mousepad"],["mouse","mousepad"],["mouse","mousepad"]]First, products are sorted lexicographically: ["mobile","moneypot","monitor","mouse","mousepad"]. For each prefix of "mouse": - Prefix "m": suggestions are the first three products starting with 'm': ["mobile","moneypot","monitor"]. - Prefix "mo": same as above. - Prefix "mou": products starting with 'mou' are ["mouse","mousepad"]. - Prefix "mous": same as above. - Prefix "mouse": same as above. The algorithm builds a trie to efficiently track these suggestions for each prefix, enabling quick retrieval of up to three lexicographically smallest matches.
Approach
Straightforward Solution
A brute-force approach would, for each prefix of searchWord, scan all products to find matches and then sort them to pick the top three. This approach is O(m * n * k log k) where m is length of searchWord, n is number of products, and k is average product length, which is inefficient for large inputs.
Core Observation
The problem requires efficient prefix-based retrieval of lexicographically smallest suggestions. A trie is a natural data structure for prefix queries, allowing incremental traversal and storage of suggestions at each node.
Path to Optimal
Sorting products lexicographically upfront allows building a trie where each node maintains up to three suggestions in sorted order. Inserting products in sorted order ensures suggestions are always the lexicographically smallest. This avoids repeated sorting during queries. Traversing the trie for each prefix of searchWord yields the suggestions in O(m) time.
Optimal Approach
Sort products lexicographically. Build a trie where each node stores up to three suggestions. For each product, insert it character by character, appending it to the suggestions list of each node if the list has fewer than three items. Then, for each prefix of searchWord, traverse the trie to collect the stored suggestions. If at any point the prefix path does not exist, append empty lists for remaining prefixes. This approach achieves efficient prefix queries with precomputed suggestions.
Time
O(n * k + m)
Sorting products takes O(n log n). Building the trie takes O(n * k), where n is number of products and k is average length. Each insertion updates up to k nodes, each with O(1) append since suggestions capped at 3. Querying for m prefixes takes O(m). Overall dominated by O(n * k).
Space
O(n * k)
The trie stores nodes for all characters of all products, with each node holding up to three suggestions. This results in O(n * k) auxiliary space, excluding the input and output.
Pattern Spotlight
Trie (Prefix Tree with Stored Suggestions)
When needing to retrieve lexicographically smallest suggestions for multiple prefixes, pre-sort inputs and store limited-size suggestion lists at each trie node during insertion to enable O(1) retrieval per prefix without repeated sorting.
Solution
| 1 | class TrieNode: |
| 2 | def __init__(self): |
| 3 | self.children = {} |
| 4 | self.suggestions = [] |
| 5 | |
| 6 | class Solution: |
| 7 | def suggestedProducts(self, products: List[str], searchWord: str) -> List[List[str]]: |
| 8 | products.sort() |
| 9 | root = TrieNode() |
| 10 | |
| 11 | for word in products: |
| 12 | node = root |
| 13 | for c in word: |
| 14 | if c not in node.children: |
| 15 | node.children[c] = TrieNode() |
| 16 | node = node.children[c] |
| 17 | |
| 18 | if len(node.suggestions) < 3: |
| 19 | node.suggestions.append(word) |
| 20 | |
| 21 | res = [] |
| 22 | node = root |
| 23 | |
| 24 | for c in searchWord: |
| 25 | if node and c in node.children: |
| 26 | node = node.children[c] |
| 27 | res.append(node.suggestions) |
| 28 | else: |
| 29 | node = None |
| 30 | res.append([]) |
| 31 | |
| 32 | return res |
Step-by-Step Solution
Sort Products and Build Trie with Limited Suggestions per Node
| 8 | products.sort() |
| 9 | root = TrieNode() |
| 11 | for word in products: |
| 12 | node = root |
| 13 | for c in word: |
| 14 | if c not in node.children: |
| 15 | node.children[c] = TrieNode() |
| 16 | node = node.children[c] |
| 18 | if len(node.suggestions) < 3: |
| 19 | node.suggestions.append(word) |
Objective
To prepare the data structure by sorting products and constructing a trie that stores up to three lexicographically smallest suggestions at each node.
Key Insight
Sorting products upfront ensures that when inserting them into the trie, the first products encountered at each node are the lexicographically smallest. By appending products to the suggestions list only if it has fewer than three items, the trie nodes maintain a capped list of top suggestions without needing to sort repeatedly. This precomputation enables fast prefix queries later.
Interview Quick-Check
Core Logic
Sorting products before insertion guarantees that suggestions stored at each trie node are lexicographically ordered and limited to three, enabling O(1) retrieval.
State & Boundaries
Each trie node stores a dictionary of children and a list of up to three suggestions, ensuring bounded memory per node.
Common Pitfalls & Bugs
Failing to limit suggestions to three per node can cause excessive memory usage and degrade performance.
Complexity
Building the trie takes O(n * k) time, where n is number of products and k is average product length.
Traverse Trie to Collect Suggestions for Each Prefix of SearchWord
| 21 | res = [] |
| 22 | node = root |
| 24 | for c in searchWord: |
| 25 | if node and c in node.children: |
| 26 | node = node.children[c] |
| 27 | res.append(node.suggestions) |
| 28 | else: |
| 29 | node = None |
| 30 | res.append([]) |
Objective
To efficiently retrieve up to three suggestions for each prefix of searchWord by traversing the trie nodes corresponding to each character.
Key Insight
By following the trie nodes for each character of searchWord, the algorithm accesses the precomputed suggestions stored at each node. If at any point the path does not exist, it appends empty lists for the remaining prefixes, ensuring correct output length. This traversal avoids repeated searches or sorting, providing O(m) query time where m is length of searchWord.
Interview Quick-Check
Core Logic
The traversal uses the trie structure to directly access stored suggestions for each prefix, avoiding repeated filtering or sorting.
State & Boundaries
If a character is missing in the current node's children, the traversal stops and empty lists are appended for all remaining prefixes.
Common Pitfalls & Bugs
Not resetting the node to None after a missing child leads to incorrect suggestions for subsequent prefixes.
Complexity
The prefix traversal takes O(m) time, where m is the length of searchWord.
Return the Aggregated List of Suggestions for All Prefixes
| 32 | return res |
Objective
To output the final list of suggestion lists corresponding to each prefix of searchWord.
Key Insight
After traversing the trie for all prefixes, the accumulated list contains the required suggestions. Returning this list completes the solution, matching the problem's output format.
Interview Quick-Check
Core Logic
Returning the collected suggestions list directly provides the required output format.
Line Analysis
This solution has 5 Critical lines interviewers watch for.
self.children = {}
Initialize a dictionary that maps characters to child TrieNode objects.
Each trie node must maintain references to its next characters so prefix traversal can proceed efficiently. Using a dictionary keyed by character enables constant-time lookup when inserting products and when traversing prefixes during queries.
self.suggestions = []
Initialize a list to store up to three product suggestions for this prefix.
Each node caches up to three lexicographically smallest products that share the prefix represented by this node. Storing these suggestions during trie construction allows prefix queries to return results instantly without searching or sorting at query time.
products.sort()
Sort the products array lexicographically.
Sorting ensures that when products are inserted into the trie, suggestions stored at each node are the lexicographically smallest, enabling efficient retrieval without further sorting.
if len(node.suggestions) < 3:
Check if the suggestions list at the current node has fewer than three products.
Limiting suggestions to three ensures memory efficiency and meets problem constraints for output size.
node.suggestions.append(word)
Append the current product to the suggestions list at the current node.
Appending products in sorted order guarantees that the suggestions list contains the lexicographically smallest products for the prefix represented by this node.
node.children[c] = TrieNode()
Create a new trie node for the current character if missing.
Adding new nodes ensures the trie accurately represents all prefixes of inserted products.
if node and c in node.children:
Check if current node exists and contains the current character as a child.
Verifying child existence ensures traversal continues only along valid prefix paths in the trie.
res.append(node.suggestions)
Append the suggestions list at the current node to the result.
Collecting the precomputed suggestions for the current prefix enables efficient retrieval without additional filtering or sorting.
else:
Handle the case where the prefix path does not exist in the trie.
Setting node to None signals that no further valid prefixes exist, ensuring subsequent prefixes yield empty suggestions.
root = TrieNode()
Create the root node of the trie.
The root node serves as the starting point for all insertions and prefix traversals, anchoring the trie structure.
if c not in node.children:
Check if the current character is not a child of the current node.
Detecting missing children nodes triggers creation of new trie nodes, expanding the trie to accommodate new prefixes.
node = node.children[c]
Move to the child node corresponding to the current character.
Advancing the node pointer allows insertion to proceed deeper into the trie along the product's prefix path.
node = root
Start traversal from the root node for prefix queries.
Beginning at the root allows traversal down the trie to find suggestions for each prefix of searchWord.
node = node.children[c]
Move to the child node corresponding to the current character.
Advancing the node pointer follows the prefix path to retrieve suggestions for the current prefix.
node = None
Set node to None to indicate no further traversal is possible.
This prevents invalid accesses and ensures empty suggestion lists for all remaining prefixes.
res.append([])
Append an empty list to the result for prefixes with no matches.
Appending empty lists maintains the output length and format, indicating no suggestions for these prefixes.
for word in products:
Iterate over each product to insert it into the trie.
Processing each product allows building the trie incrementally, ensuring all products contribute to the prefix suggestions.
node = root
Start insertion from the root node for the current product.
Resetting to root for each product ensures insertion begins at the top of the trie, correctly building the prefix paths.
for c in word:
Iterate over each character in the current product.
Processing characters sequentially builds the trie path representing the product's prefix structure.
res = []
Initialize the result list to store suggestions for each prefix.
This list accumulates the output for each prefix of searchWord, matching the problem's required return format.
for c in searchWord:
Iterate over each character in searchWord to build prefixes.
Processing each character incrementally forms prefixes for which suggestions are retrieved.
return res
Return the list of suggestion lists for all prefixes of searchWord.
Returning the accumulated results completes the solution, providing the required output format.
Test Your Understanding
Why does storing up to three suggestions at each trie node during insertion guarantee lexicographically smallest suggestions for any prefix?
Because products are inserted in lexicographical order, the first three products that reach a node are the lexicographically smallest with that prefix. By limiting the suggestions list to three, the node always holds the top three smallest products for that prefix.
Related Problems
Tries pattern
Don't just read it. Drill it.
Reconstruct Search Suggestions System from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.
Unlock the Search Suggestions System drill