Facebook Pixel

1268. Search Suggestions System

Problem Description

You need to build a product search suggestion system. Given an array of product names products and a search word searchWord, your system should suggest products as the user types each character of the search word.

The requirements are:

  • After typing each character of searchWord, suggest up to 3 products from products that start with the currently typed prefix
  • If more than 3 products match the prefix, return the 3 that come first lexicographically (alphabetically)
  • Return the suggestions as a list of lists, where each inner list contains the suggestions after typing each character

For example, if products = ["mobile", "mouse", "moneypot", "monitor", "mousepad"] and searchWord = "mouse":

  • After typing "m": suggest ["mobile", "moneypot", "monitor"] (3 products starting with "m", sorted alphabetically)
  • After typing "mo": suggest ["mobile", "moneypot", "monitor"] (3 products starting with "mo")
  • After typing "mou": suggest ["mouse", "mousepad"] (only 2 products start with "mou")
  • After typing "mous": suggest ["mouse", "mousepad"]
  • After typing "mouse": suggest ["mouse", "mousepad"]

The solution uses a Trie data structure combined with sorting. First, the products are sorted alphabetically. Then a Trie is built where each node stores up to 3 product indices that have the corresponding prefix. When searching, for each character typed, the Trie traversal finds the matching products efficiently. The indices stored in the Trie nodes are then mapped back to the actual product names from the sorted products array.

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

Intuition

When we need to find products that share a common prefix with what's being typed, the natural data structure that comes to mind is a Trie (prefix tree). A Trie excels at prefix matching because each path from the root to a node represents a prefix, and we can efficiently traverse character by character.

However, we also need to return the lexicographically smallest products when there are more than 3 matches. Rather than sorting products at each node during search time, we can pre-sort the entire products array once. This way, when we insert products into the Trie in sorted order, the first products we encounter for any prefix are automatically the lexicographically smallest ones.

The key insight is to store indices instead of actual product names in the Trie nodes. Since we've pre-sorted the products array, if we store indices [i, j, k] where i < j < k, we know that products[i] comes before products[j] lexicographically, which comes before products[k].

By limiting each Trie node to store at most 3 indices, we automatically maintain the "top 3 lexicographically smallest" products for each prefix. As we build the Trie by inserting products in sorted order, the first 3 products that pass through any node are guaranteed to be the 3 smallest for that prefix.

During the search phase, we simply traverse the Trie following the characters of searchWord. At each step, the current node already contains the indices of up to 3 best matching products, which we can directly add to our result. If at any point a character doesn't exist in the Trie, we know there are no matching products for that prefix and all subsequent prefixes, so we can stop early.

Learn more about Trie, Binary Search, Sorting and Heap (Priority Queue) patterns.

Solution Approach

The implementation consists of two main components: a Trie data structure and the main solution class.

Trie Implementation:

Each Trie node contains:

  • children: An array of size 26 (for lowercase letters a-z), where children[i] points to the child node for character 'a' + i. Initially all are None.
  • v: A list that stores up to 3 product indices that have the prefix represented by the path to this node.

The insert method adds a word to the Trie:

def insert(self, w, i):
    node = self
    for c in w:
        idx = ord(c) - ord('a')  # Convert character to index (0-25)
        if node.children[idx] is None:
            node.children[idx] = Trie()  # Create new node if needed
        node = node.children[idx]
        if len(node.v) < 3:
            node.v.append(i)  # Store product index (max 3)

For each character in the word, we calculate its index (0-25), create a child node if it doesn't exist, move to that child, and add the product index if we haven't stored 3 products yet.

The search method finds suggestions for each prefix:

def search(self, w):
    node = self
    ans = [[] for _ in range(len(w))]  # Initialize result for each character
    for i, c in enumerate(w):
        idx = ord(c) - ord('a')
        if node.children[idx] is None:
            break  # No products with this prefix
        node = node.children[idx]
        ans[i] = node.v  # Get stored indices for this prefix
    return ans

We traverse the Trie following the search word. If we can't find a character, we stop (remaining results stay empty). Otherwise, we collect the stored indices at each node.

Main Solution:

  1. Sort the products: products.sort() ensures lexicographic ordering.

  2. Build the Trie: Insert each product with its index in the sorted array:

trie = Trie()
for i, w in enumerate(products):
    trie.insert(w, i)
  1. Search and map indices to products:
return [[products[i] for i in v] for v in [trie](/problems/trie_intro).search(searchWord)]

The search returns indices for each prefix, which we map back to actual product names.

Time Complexity:

  • Building Trie: O(N * M) where N is the number of products and M is the average length of products
  • Sorting: O(N * log N * M) for comparing strings
  • Searching: O(L) where L is the length of searchWord
  • Total: O(N * log N * M + N * M + L)

Space Complexity: O(N * M) for storing the Trie structure.

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 with products = ["apple", "apricot", "application"] and searchWord = "app".

Step 1: Sort the products alphabetically After sorting: ["apple", "application", "apricot"]

  • Indices: apple → 0, application → 1, apricot → 2

Step 2: Build the Trie

Insert "apple" (index 0):

root
  └─ a: [0]
      └─ p: [0]
          └─ p: [0]
              └─ l: [0]
                  └─ e: [0]

Insert "application" (index 1):

root
  └─ a: [0,1]
      └─ p: [0,1]
          └─ p: [0,1]
              └─ l: [0,1]
                  ├─ e: [0]
                  └─ i: [1]
                      └─ c: [1]
                          └─ a: [1]
                              └─ t: [1]
                                  └─ i: [1]
                                      └─ o: [1]
                                          └─ n: [1]

Insert "apricot" (index 2):

root
  └─ a: [0,1,2]
      └─ p: [0,1,2]
          ├─ p: [0,1]
          │   └─ l: [0,1]
          │       ├─ e: [0]
          │       └─ i: [1]...
          └─ r: [2]
              └─ i: [2]
                  └─ c: [2]
                      └─ o: [2]
                          └─ t: [2]

Step 3: Search for "app"

  • Type 'a': Traverse to node 'a', which stores [0,1,2]

    • Map to products: ["apple", "application", "apricot"]
  • Type 'p' (prefix "ap"): Traverse to node 'p' under 'a', which stores [0,1,2]

    • Map to products: ["apple", "application", "apricot"]
  • Type 'p' (prefix "app"): Traverse to node 'p' under 'ap', which stores [0,1]

    • Map to products: ["apple", "application"]

Final Result: [["apple", "application", "apricot"], ["apple", "application", "apricot"], ["apple", "application"]]

Each inner list represents the suggestions after typing each character. Notice how:

  • The Trie stores at most 3 indices per node
  • Products are suggested in lexicographic order because we sorted first
  • Only products with the exact prefix are returned (e.g., "apricot" doesn't match "app")

Solution Implementation

1from typing import List, Optional
2
3class Trie:
4    def __init__(self):
5        # Array to store 26 possible child nodes (one for each lowercase letter)
6        self.children: List[Optional['Trie']] = [None] * 26
7        # Store indices of up to 3 products that have this prefix
8        self.product_indices: List[int] = []
9
10    def insert(self, word: str, product_index: int) -> None:
11        """Insert a word into the trie with its product index."""
12        current_node = self
13      
14        for char in word:
15            # Calculate the index for this character (0-25)
16            char_index = ord(char) - ord('a')
17          
18            # Create a new node if it doesn't exist
19            if current_node.children[char_index] is None:
20                current_node.children[char_index] = Trie()
21          
22            # Move to the child node
23            current_node = current_node.children[char_index]
24          
25            # Store up to 3 product indices at each node
26            if len(current_node.product_indices) < 3:
27                current_node.product_indices.append(product_index)
28
29    def search(self, word: str) -> List[List[int]]:
30        """Search for product suggestions for each prefix of the given word."""
31        current_node = self
32        # Initialize result list with empty lists for each character position
33        suggestions = [[] for _ in range(len(word))]
34      
35        for position, char in enumerate(word):
36            # Calculate the index for this character
37            char_index = ord(char) - ord('a')
38          
39            # If no child exists for this character, no more matches possible
40            if current_node.children[char_index] is None:
41                break
42          
43            # Move to the child node
44            current_node = current_node.children[char_index]
45            # Store the product indices for this prefix
46            suggestions[position] = current_node.product_indices
47      
48        return suggestions
49
50
51class Solution:
52    def suggestedProducts(
53        self, products: List[str], searchWord: str
54    ) -> List[List[str]]:
55        """
56        Return suggested products for each prefix of searchWord.
57        For each prefix, return up to 3 lexicographically smallest products.
58        """
59        # Sort products lexicographically
60        products.sort()
61      
62        # Build the trie with all products
63        trie = Trie()
64        for index, product in enumerate(products):
65            trie.insert(product, index)
66      
67        # Get suggestions for each prefix and convert indices to product names
68        suggestion_indices = trie.search(searchWord)
69        return [[products[index] for index in indices] for indices in suggestion_indices]
70
1/**
2 * Trie data structure for efficient prefix-based search
3 * Each node stores up to 3 product indices for suggestions
4 */
5class Trie {
6    // Array of child nodes for each lowercase letter (a-z)
7    Trie[] children = new Trie[26];
8    // List to store indices of products (max 3) that have this prefix
9    List<Integer> productIndices = new ArrayList<>();
10
11    /**
12     * Inserts a word into the trie with its corresponding product index
13     * @param word The product name to insert
14     * @param index The index of the product in the sorted products array
15     */
16    public void insert(String word, int index) {
17        Trie currentNode = this;
18      
19        // Traverse through each character in the word
20        for (int i = 0; i < word.length(); i++) {
21            // Calculate the index for the character (0-25 for a-z)
22            int charIndex = word.charAt(i) - 'a';
23          
24            // Create a new node if it doesn't exist
25            if (currentNode.children[charIndex] == null) {
26                currentNode.children[charIndex] = new Trie();
27            }
28          
29            // Move to the child node
30            currentNode = currentNode.children[charIndex];
31          
32            // Store the product index (maximum 3 products per node)
33            if (currentNode.productIndices.size() < 3) {
34                currentNode.productIndices.add(index);
35            }
36        }
37    }
38
39    /**
40     * Searches for product suggestions for each prefix of the search word
41     * @param searchWord The word to search for
42     * @return Array of lists containing product indices for each prefix
43     */
44    public List<Integer>[] search(String searchWord) {
45        Trie currentNode = this;
46        int wordLength = searchWord.length();
47      
48        // Initialize result array with empty lists for each character position
49        List<Integer>[] results = new List[wordLength];
50        Arrays.setAll(results, i -> new ArrayList<>());
51      
52        // Traverse through each character of the search word
53        for (int i = 0; i < wordLength; i++) {
54            int charIndex = searchWord.charAt(i) - 'a';
55          
56            // If no matching child exists, stop searching
57            if (currentNode.children[charIndex] == null) {
58                break;
59            }
60          
61            // Move to the child node and store its product indices
62            currentNode = currentNode.children[charIndex];
63            results[i] = currentNode.productIndices;
64        }
65      
66        return results;
67    }
68}
69
70/**
71 * Solution class for the product suggestion system problem
72 */
73class Solution {
74    /**
75     * Returns suggested products for each prefix of the search word
76     * @param products Array of product names
77     * @param searchWord The word to search for
78     * @return List of lists containing suggested product names for each prefix
79     */
80    public List<List<String>> suggestedProducts(String[] products, String searchWord) {
81        // Sort products lexicographically to ensure suggestions are in order
82        Arrays.sort(products);
83      
84        // Build the trie with all products
85        Trie trie = new Trie();
86        for (int i = 0; i < products.length; i++) {
87            trie.insert(products[i], i);
88        }
89      
90        // Get product indices for each prefix of the search word
91        List<Integer>[] productIndicesArray = trie.search(searchWord);
92      
93        // Convert indices to actual product names
94        List<List<String>> suggestions = new ArrayList<>();
95        for (List<Integer> indices : productIndicesArray) {
96            List<String> productNames = new ArrayList<>();
97            for (int productIndex : indices) {
98                productNames.add(products[productIndex]);
99            }
100            suggestions.add(productNames);
101        }
102      
103        return suggestions;
104    }
105}
106
1class Trie {
2public:
3    /**
4     * Insert a word into the trie with its index in the products array
5     * At each node, store up to 3 product indices (lexicographically smallest)
6     * @param word: the product word to insert
7     * @param productIndex: the index of this product in the sorted products array
8     */
9    void insert(string& word, int productIndex) {
10        Trie* currentNode = this;
11      
12        // Traverse each character in the word
13        for (int charPos = 0; charPos < word.size(); ++charPos) {
14            int charIndex = word[charPos] - 'a';
15          
16            // Create new node if path doesn't exist
17            if (!currentNode->children[charIndex]) {
18                currentNode->children[charIndex] = new Trie();
19            }
20          
21            // Move to next node
22            currentNode = currentNode->children[charIndex];
23          
24            // Store up to 3 product indices at this node
25            if (currentNode->productIndices.size() < 3) {
26                currentNode->productIndices.push_back(productIndex);
27            }
28        }
29    }
30
31    /**
32     * Search for suggestions for each prefix of the search word
33     * @param searchWord: the word to search for
34     * @return: a 2D vector where ans[i] contains product indices for prefix searchWord[0..i]
35     */
36    vector<vector<int>> search(string& searchWord) {
37        Trie* currentNode = this;
38        int wordLength = searchWord.size();
39        vector<vector<int>> suggestions(wordLength);
40      
41        // For each character in searchWord, find matching products
42        for (int charPos = 0; charPos < searchWord.size(); ++charPos) {
43            int charIndex = searchWord[charPos] - 'a';
44          
45            // If no matching path exists, remaining suggestions will be empty
46            if (!currentNode->children[charIndex]) {
47                break;
48            }
49          
50            // Move to next node and get its stored product indices
51            currentNode = currentNode->children[charIndex];
52            suggestions[charPos] = move(currentNode->productIndices);
53        }
54      
55        return suggestions;
56    }
57
58private:
59    // Array of 26 pointers for each lowercase letter
60    vector<Trie*> children = vector<Trie*>(26);
61  
62    // Stores up to 3 product indices at this node
63    vector<int> productIndices;
64};
65
66class Solution {
67public:
68    /**
69     * Find suggested products for each prefix of searchWord
70     * Returns at most 3 lexicographically smallest products for each prefix
71     * @param products: list of available products
72     * @param searchWord: the word being typed/searched
73     * @return: suggestions for each prefix of searchWord
74     */
75    vector<vector<string>> suggestedProducts(vector<string>& products, string searchWord) {
76        // Sort products lexicographically
77        sort(products.begin(), products.end());
78      
79        // Build trie with all products
80        Trie* trie = new Trie();
81        for (int i = 0; i < products.size(); ++i) {
82            trie->insert(products[i], i);
83        }
84      
85        // Get suggestions for each prefix of searchWord
86        vector<vector<string>> result;
87        vector<vector<int>> suggestionIndices = trie->search(searchWord);
88      
89        // Convert product indices back to product names
90        for (auto& indices : suggestionIndices) {
91            vector<string> suggestions;
92            for (int index : indices) {
93                suggestions.push_back(products[index]);
94            }
95            result.push_back(move(suggestions));
96        }
97      
98        return result;
99    }
100};
101
1/**
2 * Trie node structure for storing product suggestions
3 */
4interface TrieNode {
5    // Array of 26 children nodes for each lowercase letter
6    children: (TrieNode | null)[];
7    // Stores up to 3 product indices at this node
8    productIndices: number[];
9}
10
11/**
12 * Creates a new trie node
13 * @returns A new trie node with empty children and product indices
14 */
15function createTrieNode(): TrieNode {
16    return {
17        children: new Array(26).fill(null),
18        productIndices: []
19    };
20}
21
22/**
23 * Insert a word into the trie with its index in the products array
24 * At each node, store up to 3 product indices (lexicographically smallest)
25 * @param root - The root node of the trie
26 * @param word - The product word to insert
27 * @param productIndex - The index of this product in the sorted products array
28 */
29function insert(root: TrieNode, word: string, productIndex: number): void {
30    let currentNode = root;
31  
32    // Traverse each character in the word
33    for (let charPos = 0; charPos < word.length; charPos++) {
34        const charIndex = word.charCodeAt(charPos) - 'a'.charCodeAt(0);
35      
36        // Create new node if path doesn't exist
37        if (!currentNode.children[charIndex]) {
38            currentNode.children[charIndex] = createTrieNode();
39        }
40      
41        // Move to next node
42        currentNode = currentNode.children[charIndex]!;
43      
44        // Store up to 3 product indices at this node
45        if (currentNode.productIndices.length < 3) {
46            currentNode.productIndices.push(productIndex);
47        }
48    }
49}
50
51/**
52 * Search for suggestions for each prefix of the search word
53 * @param root - The root node of the trie
54 * @param searchWord - The word to search for
55 * @returns A 2D array where result[i] contains product indices for prefix searchWord[0..i]
56 */
57function search(root: TrieNode, searchWord: string): number[][] {
58    let currentNode = root;
59    const wordLength = searchWord.length;
60    const suggestions: number[][] = new Array(wordLength).fill(null).map(() => []);
61  
62    // For each character in searchWord, find matching products
63    for (let charPos = 0; charPos < searchWord.length; charPos++) {
64        const charIndex = searchWord.charCodeAt(charPos) - 'a'.charCodeAt(0);
65      
66        // If no matching path exists, remaining suggestions will be empty
67        if (!currentNode.children[charIndex]) {
68            break;
69        }
70      
71        // Move to next node and get its stored product indices
72        currentNode = currentNode.children[charIndex]!;
73        suggestions[charPos] = [...currentNode.productIndices];
74    }
75  
76    return suggestions;
77}
78
79/**
80 * Find suggested products for each prefix of searchWord
81 * Returns at most 3 lexicographically smallest products for each prefix
82 * @param products - List of available products
83 * @param searchWord - The word being typed/searched
84 * @returns Suggestions for each prefix of searchWord
85 */
86function suggestedProducts(products: string[], searchWord: string): string[][] {
87    // Sort products lexicographically
88    const sortedProducts = [...products].sort();
89  
90    // Build trie with all products
91    const trieRoot = createTrieNode();
92    for (let i = 0; i < sortedProducts.length; i++) {
93        insert(trieRoot, sortedProducts[i], i);
94    }
95  
96    // Get suggestions for each prefix of searchWord
97    const result: string[][] = [];
98    const suggestionIndices = search(trieRoot, searchWord);
99  
100    // Convert product indices back to product names
101    for (const indices of suggestionIndices) {
102        const suggestions: string[] = [];
103        for (const index of indices) {
104            suggestions.push(sortedProducts[index]);
105        }
106        result.push(suggestions);
107    }
108  
109    return result;
110}
111

Time and Space Complexity

Time Complexity: O(L × log n + m)

  • Sorting the products array takes O(n × log n × k) where k is the average length of product strings. This can be simplified to O(L × log n) where L is the total length of all product strings.
  • Building the Trie requires iterating through all products and inserting each character, which takes O(L) time total.
  • Each node stores at most 3 indices in the v list, and appending to this list is O(1).
  • Searching in the Trie for the searchWord takes O(m) where m is the length of searchWord.
  • Converting the indices back to product strings takes O(m) since each prefix can have at most 3 suggestions.
  • Overall time complexity: O(L × log n + L + m) = O(L × log n + m) (since L × log n dominates L).

Space Complexity: O(L)

  • The Trie structure can have at most O(L) nodes in the worst case, where each character of every product creates a new node.
  • Each node contains an array of 26 pointers to children and a list v that stores at most 3 indices.
  • The space for storing indices is O(L) in total across all nodes (at most 3 indices per node, and O(L) nodes).
  • The result list for search operation uses O(m) space temporarily.
  • Overall space complexity: O(L).

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

Common Pitfalls

1. Forgetting to Handle Empty Prefixes When No Matches Exist

A common mistake is not properly handling the case when a character in the search word doesn't exist in the Trie. When we encounter a character that has no corresponding child node, we break out of the loop, but the remaining positions in the result list stay as empty lists (which is correct). However, developers often make mistakes by:

  • Trying to continue traversal after hitting a None child
  • Not initializing the result list properly with empty lists
  • Attempting to access node.product_indices when node is None

Incorrect Implementation:

def search(self, word: str) -> List[List[int]]:
    current_node = self
    suggestions = []  # Wrong: not pre-initialized with empty lists
  
    for position, char in enumerate(word):
        char_index = ord(char) - ord('a')
        current_node = current_node.children[char_index]  # Dangerous: might be None
        suggestions.append(current_node.product_indices)  # Will crash if current_node is None
  
    return suggestions

Correct Implementation:

def search(self, word: str) -> List[List[int]]:
    current_node = self
    suggestions = [[] for _ in range(len(word))]  # Pre-initialize with empty lists
  
    for position, char in enumerate(word):
        char_index = ord(char) - ord('a')
        if current_node.children[char_index] is None:
            break  # Stop searching, remaining positions stay empty
        current_node = current_node.children[char_index]
        suggestions[position] = current_node.product_indices
  
    return suggestions

2. Adding Product Indices Without Checking the 3-Product Limit

Another pitfall is forgetting to limit the number of products stored at each Trie node to 3. Without this check, all matching products would be stored, defeating the purpose of the optimization and potentially returning more than 3 suggestions.

Incorrect Implementation:

def insert(self, word: str, product_index: int) -> None:
    current_node = self
  
    for char in word:
        char_index = ord(char) - ord('a')
        if current_node.children[char_index] is None:
            current_node.children[char_index] = Trie()
        current_node = current_node.children[char_index]
        current_node.product_indices.append(product_index)  # Wrong: no limit check

Correct Implementation:

def insert(self, word: str, product_index: int) -> None:
    current_node = self
  
    for char in word:
        char_index = ord(char) - ord('a')
        if current_node.children[char_index] is None:
            current_node.children[char_index] = Trie()
        current_node = current_node.children[char_index]
        if len(current_node.product_indices) < 3:  # Check limit before adding
            current_node.product_indices.append(product_index)

3. Inserting Products Before Sorting

A critical mistake is building the Trie before sorting the products array. Since we're storing indices and want the lexicographically smallest products, the indices must correspond to the sorted array.

Incorrect Implementation:

def suggestedProducts(self, products: List[str], searchWord: str) -> List[List[str]]:
    trie = Trie()
  
    # Wrong: Building trie before sorting
    for index, product in enumerate(products):
        trie.insert(product, index)
  
    products.sort()  # Sorting after building trie breaks index mapping
  
    suggestion_indices = trie.search(searchWord)
    return [[products[index] for index in indices] for indices in suggestion_indices]

Correct Implementation:

def suggestedProducts(self, products: List[str], searchWord: str) -> List[List[str]]:
    # Sort first to ensure indices correspond to lexicographic order
    products.sort()
  
    trie = Trie()
    for index, product in enumerate(products):
        trie.insert(product, index)
  
    suggestion_indices = trie.search(searchWord)
    return [[products[index] for index in indices] for indices in suggestion_indices]

These pitfalls can lead to runtime errors, incorrect results, or inefficient solutions. Always ensure proper null checking, enforce constraints (like the 3-product limit), and maintain correct index-to-product mapping by sorting before building the data structure.

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

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

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

Load More