# Search Suggestions System LeetCode Solution

Difficulty Level Medium

## Problem Statement

Search Suggestions System LeetCode Solution – You are given an array of strings `products` and a string `searchWord`.

Design a system that suggests at most three product names from `products` after each character of `searchWord` is typed. Suggested products should have a common prefix with `searchWord`. If there are more than three products with a common prefix return the three lexicographically minimums products.

Return a list of lists of the suggested products after each character of `searchWord` is typed.

## Example

### Input:

searchWord = “mouse”

## Explanation

After typing m and mo all products match and we show user [“mobile”,”moneypot”,”monitor”]

After typing mou, mous and mouse the system suggests [“mouse”,”mousepad”]

## TrieNode design

Now let us think about how we will process nodes in lexicographical order. At that stage, the crucial role in plays design of the TrieNode data structure.

First, we can save the order of child nodes by using an array with the same length as the English alphabet. However, the disadvantage of that solution is that we use a lot of redundant space because most of the array’s positions can be empty.

Second, we can use a hash table. That way we will get rid of the array redundancy. To explore the child nodes in lexicographical order we will have to iterate through the alphabet and check if a certain letter exists as a key in the dictionary. That way we will spend in a worst-case just `O(26)` time to check all possible keys.

In conclusion, we discussed two implementations of the TrieNode data structure and their trade-offs. The second solution uses much lesser space than the first one. But the time needed in the worst case remains the same `O(26)`.

## Prefix match queries

I think we a doing great so far. Now, let us go one step further. To find any number of words on each level of the prefix match we have to explore the tree where the current node of the Trie is the root. To do so we have to use a tree-traversal algorithm, such as DFS. In the worst case, a recursive DFS-call will take `O(n * k * s)` time and `O(n)` auxiliary space. That is a lot of work, isn’t it?

To save that time and space we can use an array `node.ids` to store indices of words from the original collection of words whose prefixes match the path to the current node in the Trie. That way to find words with a specific prefix takes constant time. All we have to do is to iterate through the `node.ids` array and return the corresponding values from the original collection.

Seems that this change saved us a lot of time and space. However, one more time by solving one problem we just created another. The order of words’ indices in `node.ids` array must represent the words in lexicographical order. To maintain the order we have to sort the initial collection `words` and add words into the Trie in that order.

Idea:

Idea is to iterate through the products and store the prefixes into a Trie.
Important Points:

• The Trie Node will hold at most 3 words, and those words will be the lexicographically minimum words. This is optimized for fast search word keystrokes results.
• A Heap will be used to ensure the 3 words that are stored in the Trie nodes will be the lexicographically minimum words.
• When all of the previous is completed, each search word keystroke will be an optimized O(1) constant operation.

## Code for Search Suggestions System

### Java Program

```class Solution {
public List<List<String>> suggestedProducts(String[] products, String searchWord) {
Trie trie = new Trie();
trie.insert(product);                                                           // by inserting each product to trie
}
return trie.search(trie, 3, searchWord, new ArrayList<List<String>>() );            // search in Trie, and keep updating final list
}

// ==================================================== Trie data-structure ====================================================== //
class Trie {
TrieNode root;
public Trie() {                                                                     // Trie Constructor
root = new TrieNode(' ');                                                       // Create root node, with ' ' as val.
}

// -----------------------------------------------------------------
public List<List<String>> search(Trie trie, int count, String searchWord, List<List<String>> resultList){         // search in Trie, and keep updating final list
for(int i = 1; i <= searchWord.length(); i++){
List<String> list = new ArrayList<String>();
TrieNode node = trie.root;

String typedSoFar = searchWord.substring(0, i);
node = reachLastCharOfPrefix(typedSoFar, count, node );                      // call method to first reach last node of current substring (prefix)
list = depthFirstSearch(node, list, count);                                  // call *DFS* to search further on all chilren of node
}
return resultList;
}

// -----------------------------------------------------------------
public List<String> depthFirstSearch(TrieNode node, List<String> list, int count){
if(node == null || list.size() >= count){ return list; }                          // if node is null, or, List got 3 strings by now, return list.  Don't forget "node == null"
if(! node.word.isEmpty()){ list.add(node.word); }                                 // if node's word is not Empty, we have found the word. add it to list, and continue further
for(TrieNode childNode : node.children){
if(childNode == null){ continue; }
list = depthFirstSearch(childNode, list, count);                              // call DFS/recursion on childNode
}
return list;
}

// -----------------------------------------------------------------
public TrieNode reachLastCharOfPrefix(String prefix, int count, TrieNode node) {
for(int i = 0; i < prefix.length(); i++){
char ch = prefix.charAt(i);
if(node.children[ch - 'a'] == null){ return null; }
node = node.children[ch - 'a'];
}
return node;
}

// -----------------------------------------------------------------
public void insert(String word) {                                                     // Let's we are inserting word "mobile"
TrieNode node = this.root;
for(int i = 0; i < word.length(); i++){                                           // loop through each char of word
char ch = word.charAt(i);                                                     // let's say first char is 'm' which is to be inserted
if(node.children[ch - 'a'] == null){                                          // if child exists at children[m - 'a']. children[109-97] = children[12]
node.children[ch - 'a'] = new TrieNode(ch);                               // then insert new TrieNode object of value 'm' at children[12]
}
node = node.children[ch - 'a'];                                               // go to next child node
}
node.word = word;
}

// ========================= class TrieNode ======================== //
static class TrieNode{
char val;                                                                         // node's char value e.g. 'm'
TrieNode[] children;                                                              // each node can have upto 26 children.
String word = "";
TrieNode(char val){                                                               // pass val in the constructor
this.val = val;
children = new TrieNode[26];                                                  // because every child is of char type. we have 26 characters.
}
}
}

}```

### Python Program

```from collections import defaultdict
from heapq import heappush, heappop
class MinWord:
def __init__(self, word):
self.word = word

# Update the comparator here so we are retaining the lexographically
# smaller words as the heap is a min_heap
def __lt__(self, other):
return self.word > other.word

class TrieNode:
def __init__(self):
self.children = defaultdict(TrieNode)
self.words = [] # Heap that will store at most 3 words

class Trie:
def __init__(self):
self.root = TrieNode()

# O(n) (n = length of the word) time complexity
n = self.root
for char in word:
n = n.children[char]
heappush(n.words, MinWord(word)) # TODO i mistakenly flipped these 2 vars
if len(n.words) > 3:
heappop(n.words)

class Solution:
# O(m*n + s) (m = length of the products, n = length of a word, s = length of search word) time complexity
def suggestedProducts(self, products: "List[str]", searchWord: str) -> "List[List[str]]":
trie = Trie()
suggestions = []
for product in products: # O(m*n) (m = length of the products) time complexity
trie.add_word(product) # O(n) (n = length of the word) time complexity

n = trie.root
for key in searchWord: # O(s) (s = length of the search word) time complexity
n = n.children[key]
suggestions.append(sorted([min_word.word for min_word in n.words]))

return suggestions```

## Complexity Analysis for Search Suggestions System LeetCode Solution

Time: O(m * n + s * k). To construct the Trie data structure we have to iterate throughout all words which takes time proportional to `O(m * n)`. After creating the Trie we will iterate over all characters in `searchWord` and on each step, we will make at most `k` iterations to collect the matched words. In our case, the `k = 3` and in terms of BigO notation can be dropped. But in a case where there are no limits or `k` is an argument, we will have to clearly specify it.

Space: O(26 ** n + s * k). Trie data structure takes at most `O(26 ** n` space because each node can have at most 26 child nodes. The depth of the Trie is the maximum length of the word. Also, we have to pay for extra `O(s * k)` space to store the result.

Translate »