Search Suggestions System LeetCode Solution

Difficulty Level Medium
Frequently asked in Adobe Amazon Apple Bloomberg Facebook Goldman Sachs Google Microsoft OracleViews 4365

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

Test Case 1:

Input:

products = [“mobile”,”mouse”,”moneypot”,”monitor”,”mousepad”]

searchWord = “mouse”

Output:

[ [“mobile”,”moneypot”,”monitor”], [“mobile”,”moneypot”,”monitor”], [“mouse”,”mousepad”], [“mouse”,”mousepad”], [“mouse”,”mousepad”] ]

Explanation

products sorted lexicographically =[“mobile”,”moneypot”,”monitor”,”mouse”,”mousepad”].

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”]

Approach:

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.

Search Suggestions System LeetCode Solution

Code for Search Suggestions System

Java Program

class Solution {
    public List<List<String>> suggestedProducts(String[] products, String searchWord) {
    Trie trie = new Trie();
    for(String product : products){                                                     // update trie data-structure with all products,
      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
        resultList.add(list); 
      }
      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
    def add_word(self, word):
        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 »