package design; import java.util.*; /** * Created by gouthamvidyapradhan on 20/08/2017. *

* Design a search autocomplete system for a search engine. Users may input a sentence (at least one word and end with a special character '#'). * For each character they type except '#', you need to return the top 3 historical hot sentences that have prefix the same as the part of sentence already typed. * Here are the specific rules: *

* The hot degree for a sentence is defined as the number of times a user typed the exactly same sentence before. * The returned top 3 hot sentences should be sorted by hot degree (The first is the hottest one). * If several sentences have the same degree of hot, you need to use ASCII-code order (smaller one appears first). * If less than 3 hot sentences exist, then just return as many as you can. * When the input is a special character, it means the sentence ends, and in this case, you need to return an empty list. * Your job is to implement the following functions: *

* The constructor function: *

* AutocompleteSystem(String[] sentences, int[] times): This is the constructor. * The input is historical data. Sentences is a string array consists of previously typed sentences. * Times is the corresponding times a sentence has been typed. Your system should record these historical data. *

* Now, the user wants to input a new sentence. The following function will provide the next character the user types: *

* List input(char c): The input c is the next character typed by the user. * The character will only be lower-case letters ('a' to 'z'), blank space (' ') or a special character ('#'). * Also, the previously typed sentence should be recorded in your system. * The output will be the top 3 historical hot sentences that have prefix the same as the part of sentence already typed. *

*

* Example: * Operation: AutocompleteSystem(["i love you", "island","ironman", "i love leetcode"], [5,3,2,2]) * The system have already tracked down the following sentences and their corresponding times: * "i love you" : 5 times * "island" : 3 times * "ironman" : 2 times * "i love leetcode" : 2 times * Now, the user begins another search: *

* Operation: input('i') * Output: ["i love you", "island","i love leetcode"] * Explanation: * There are four sentences that have prefix "i". Among them, "ironman" and "i love leetcode" have same hot degree. * Since ' ' has ASCII code 32 and 'r' has ASCII code 114, "i love leetcode" should be in front of "ironman". * Also we only need to output top 3 hot sentences, so "ironman" will be ignored. *

* Operation: input(' ') * Output: ["i love you","i love leetcode"] * Explanation: * There are only two sentences that have prefix "i ". *

* Operation: input('a') * Output: [] * Explanation: * There are no sentences that have prefix "i a". *

* Operation: input('#') * Output: [] * Explanation: * The user finished the input, the sentence "i a" should be saved as a historical sentence in system. * And the following input will be counted as a new search. *

* Note: * The input sentence will always start with a letter and end with '#', and only one blank space will exist between two words. * The number of complete sentences that to be searched won't exceed 100. * The length of each sentence including those in the historical data won't exceed 100. * Please use double-quote instead of single-quote when you write test cases even for a character input. * Please remember to RESET your class variables declared in class AutocompleteSystem, as static/class variables are persisted across multiple test cases. *

* Solution: Maintain a Trie (slightly modified) data-structure to all the input sentences where each node of the Trie * is a node containing a hash-map of child character and node and a TreeSet containing the sorted order of all the * possible child sentences starting from the current node. Maintain a cursor node 'curr' to indicate the current node * of input, if the input character is absent then simply mark curr as null indicating no further auto-complete terms possible. * On the other hand if the input character is present then simply pick the top 3 elements from the TreeSet object of curr * node. * Finally, use a StringBuilder to accumulate all the input characters and when a end of sentence is '#' is encountered * simply update the trie with new sentence and degree. */ public class AutocompleteSystem { private Map hotTextMap; //Maintain a hash-map of sentences and degree private Trie curr, trie, root; private StringBuilder currSentence; //StringBuilder class to maintain current input sentence (until '#') /** * Main method * * @param args * @throws Exception */ public static void main(String[] args) throws Exception { String[] sentences = {"i love you", "island", "ironman", "i love leetcode"}; int[] degree = {5, 3, 2, 2}; AutocompleteSystem autocomplete = new AutocompleteSystem(sentences, degree); List result = autocomplete.input('i'); result.forEach(System.out::println); result = autocomplete.input(' '); result.forEach(System.out::println); result = autocomplete.input('a'); result.forEach(System.out::println); result = autocomplete.input('#'); result.forEach(System.out::println); } /** * Initialize the trie data-structure * * @param sentences array of sentences * @param times degree */ public AutocompleteSystem(String[] sentences, int[] times) { hotTextMap = new HashMap<>(); trie = new Trie(); for (int i = 0; i < sentences.length; i++) { hotTextMap.put(sentences[i], times[i]); trie.insert(sentences[i]); trie.update(sentences[i]); } curr = trie; root = trie; currSentence = new StringBuilder(); } /** * Accept input and return hot-text for the current char * * @param c char * @return List of top 3 hot-text */ public List input(char c) { List result = new ArrayList<>(); if (c == '#') { String sentence = currSentence.toString(); if (hotTextMap.containsKey(sentence)) { //already a known sentence hence only update in necessary hotTextMap.put(sentence, hotTextMap.get(sentence) + 1); trie.update(sentence); } else { //insert a new sentence and update the degree hotTextMap.put(sentence, 1); trie.insert(sentence); trie.update(sentence); } currSentence = new StringBuilder(); //reset StringBuilder curr = root; //point to root of the trie } else { if (curr != null) { if (curr.containsChild(c)) { List hotText = curr.getSubtrie(c).getTop3HotText(); hotText.stream().forEach((x) -> result.add(currSentence.toString() + x)); //each node only returns //the hot-text for the current and child nodes hence we have to attach the prefix string curr = curr.getSubtrie(c); } else { curr = null; //as soon as we encounter a empty node then set current to null indicating no further //auto-complete terms are available } } currSentence.append(c); } return result; } /** * Class HotText to store the text and degree */ private class HotText { private String text; private int degree; HotText(String text, int degree) { this.text = text; this.degree = degree; } private String getText() { return text; } private int getDegree() { return degree; } } /** * Class Trie */ private class Trie { private Map map = new HashMap<>(); private TreeSet hotText = new TreeSet<>((HotText o1, HotText o2) -> { int cmp = Integer.compare(o2.getDegree(), o1.getDegree()); return cmp == 0 ? o1.getText().compareTo(o2.getText()) : cmp; }); /** * Get hot-text * * @return HotText */ public TreeSet getHotText() { return hotText; } /** * Return top 3 hottext * * @return hot text */ private List getTop3HotText() { List hotText = new ArrayList<>(); if (this.getHotText() != null) { this.getHotText().stream().limit(3).forEach((x) -> hotText.add(x.getText())); } return hotText; } /** * Inserts a word into the trie. */ private void insert(String word) { if (word != null) { add(0, word, word.length()); } } private void add(int i, String word, int length) { if (i < length) { char c = word.charAt(i); Trie subTrie = map.get(c); if (subTrie == null) { subTrie = new Trie(); map.put(c, subTrie); } subTrie.add(i + 1, word, length); } else map.put(null, new Trie()); //use null to indicate end of string } /** * Update hottex and degree * * @param word word or sentence */ private void update(String word) { if (word != null) { update(0, word, word.length()); } } /** * Update trie * * @param i curr position * @param word sentence * @param length length * @return HotText */ private HotText update(int i, String word, int length) { if (i < length) { char c = word.charAt(i); Trie subTrie = map.get(c); HotText subTrieHotText = subTrie.update(i + 1, word, length); HotText currHotText = new HotText(c + (subTrieHotText == null ? "" : subTrieHotText.getText()), hotTextMap.get(word)); updateHotText(subTrie, currHotText); return currHotText; } return null; } /** * Hot text update * * @param hotText hotText object */ private void updateHotText(Trie trie, HotText hotText) { trie.getHotText().remove(new HotText(hotText.getText(), hotText.getDegree() - 1)); //remove already // contained hot-text and add new trie.getHotText().add(hotText); } /** * Contains child * * @param c char * @return true if it contains child, false otherwise */ private boolean containsChild(char c) { return this.map.containsKey(c); } /** * Return child tree * * @param c char * @return child subTrie */ private Trie getSubtrie(char c) { return this.map.get(c); } } }