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);
}
}
}