Search Suggestions System

Medium Tries

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

Input: products = ["mobile","mouse","moneypot","monitor","mousepad"], searchWord = "mouse"
Output: [["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

1class TrieNode:
2 def __init__(self):
3 self.children = {}
4 self.suggestions = []
5
6class 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

1

Sort Products and Build Trie with Limited Suggestions per Node

8products.sort()
9root = TrieNode()
11for 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.

2

Traverse Trie to Collect Suggestions for Each Prefix of SearchWord

21res = []
22node = root
24for 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.

3

Return the Aggregated List of Suggestions for All Prefixes

32return 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.

Line 3 Critical
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.

Line 4 Critical
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.

Line 8 Critical
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.

Line 18 Critical
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.

Line 19 Critical
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.

Line 15 Core
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.

Line 25 Core
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.

Line 27 Core
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.

Line 28 Core
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.

Line 9 Core
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.

Line 14 Core
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.

Line 16 Core
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.

Line 22 Core
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.

Line 26 Core
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.

Line 29 Core
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.

Line 30 Core
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.

Line 11 Standard
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.

Line 12 Standard
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.

Line 13 Standard
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.

Line 21 Standard
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.

Line 24 Standard
for c in searchWord:

Iterate over each character in searchWord to build prefixes.

Processing each character incrementally forms prefixes for which suggestions are retrieved.

Line 32 Standard
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

or drill a free problem