LeetCode上第 642 號問題:Design Search Autocomplete System
題目描述
為搜尋引擎設計一個搜尋自動完成系統。使用者可以輸入一個句子(至少一個單詞,並以一個特殊的字元'#'結尾)。對於除'#'之外的每個字元,您需要返回與已輸入的句子部分字首相同的前3個歷史熱門句子。具體規則如下:
一個句子的熱度定義為使用者輸入完全相同句子的次數。 返回的前3個熱門句子應該按照熱門程度排序(第一個是最熱的)。如果幾個句子的熱度相同,則需要使用ascii程式碼順序(先顯示較小的一個)。 如果少於3個熱門句子,那麼就儘可能多地返回。 當輸入是一個特殊字元時,它意味著句子結束,在這種情況下,您需要返回一個空列表。 您的工作是實現以下功能:
建構函式:
AutocompleteSystem(String[] sentence, int[] times):這是建構函式。輸入是歷史資料。句子是由之前輸入的句子組成的字串陣列。Times是輸入一個句子的相應次數。您的系統應該記錄這些歷史資料。
現在,使用者想要輸入一個新句子。下面的函式將提供使用者型別的下一個字元:
List input(char c):輸入c是使用者輸入的下一個字元。字元只能是小寫字母(“a”到“z”)、空格(“”)或特殊字元(“#”)。另外,前面輸入的句子應該記錄在系統中。輸出將是前3個歷史熱門句子,它們的字首與已經輸入的句子部分相同。
例子: 操作:AutocompleteSystem(["i love you", "island","ironman", "i love leetcode"], [5,3,2,2]) 系統已經追蹤到以下句子及其對應的時間:
"i love you" : 5 times "island" : 3 times "ironman" : 2 times "i love leetcode" : 2 times
現在,使用者開始另一個搜尋:
操作:輸入(“i”) 輸出:["i love you", "island","i love leetcode"] 解釋: 有四個句子有字首“i”。其中,《ironman》和《i love leetcode》有著相同的熱度。既然“ ” ASCII碼為32,“r”ASCII碼為114,那麼“i love leetcode”應該在“ironman”前面。此外,我們只需要輸出前3個熱門句子,所以“ironman”將被忽略。
操作:輸入(' ') 輸出:[“i love you”,“i love leetcode”] 解釋: 只有兩個句子有字首“i”。
操作:輸入(' a ') 輸出:[] 解釋: 沒有以“i a”為字首的句子。
操作:輸入(“#”) 輸出:[] 解釋: 使用者完成輸入後,在系統中將句子“i a”儲存為歷史句。下面的輸入將被計算為新的搜尋。
注意:
輸入的句子總是以字母開頭,以“#”結尾,兩個單詞之間只有一個空格。 要搜尋的完整句子不會超過100個。包括歷史資料在內的每句話的長度不會超過100句。 在編寫測試用例時,即使是字元輸入,也請使用雙引號而不是單引號。 請記住重置在AutocompleteSystem類中宣告的類變數,因為靜態/類變數是跨多個測試用例持久化的。詳情請點選這裡。
題目大意:
設計一個搜尋自動補全系統,它需要包含如下兩個方法:
構造方法:
AutocompleteSystem(String[] sentences, int[] times): 輸入句子sentences,及其出現次數times
輸入方法:
List input(char c): 輸入字元c可以是26個小寫英文字母,也可以是空格,以'#'結尾。返回輸入字元字首對應頻率最高的至多3個句子,頻率相等時按字典序排列。
思路解析:
核心點:Trie(字典樹)
利用字典樹記錄所有出現過的句子集合,利用字典儲存每個句子出現的次數。
解題思路
題目的要求是補全的句子是按之前出現的頻率排列的,高頻率的出現在最上面,如果頻率相同,就按字母順序來顯示。
頻率 這種要求很容易想到 堆、優先佇列、樹、Map等知識點,這裡涉及到 字典 與 樹,那肯定使用 字典樹 能解決。
所以首先構造 Trie 的 trieNode 結構以及 insert 方法,構造完 trieNode 類後,再構造一個樹的根節點。
由於每次都要輸入一個字元,我們可以用一個私有的 Node:curNode 來追蹤當前的節點。
curNode 初始化為 root ,在每次輸入完一個句子時,即輸入的字元為‘#’時,我們需要將其置為root。
同時還需要一個 string 型別 stn 來表示當前的搜尋的句子。
每輸入一個字元,首先檢查是不是結尾標識“#”,如果是的話,將當前句子加入trie樹,重置相關變數,返回空陣列。
-
如不是,檢查當前 TrieNode 對應的 child 是否含有 c 的對應節點。如果沒有,將 curNode 置為 NULL 並且返回空陣列。
-
若存在,將curNode 更新為c對應的節點,並且對curNode進行dfs。
dfs 時,我們首先檢查當前是不是一個完整的句子,如果是,將句子與其次數同時加入 priority_queue 中,然後對其 child 中可能存在的子節點進行 dfs 。
進行完 dfs 後,只需要取出前三個,需要注意的是,可能可選擇的結果不滿3個,所以要在 while 中多加入檢測 q 為空的條件語句。
最後要將 q 中的所有元素都彈出。
動畫演示
動畫是使用 AE 製作,體積比較大,有 32 M,無法使用GIF播放,因此採取視訊播放形式,手機黨慎點:)
感謝 Jun Chen 大佬提供動畫技術支援,筆芯。
Markdown 不提供視訊播放功能,請前往這裡進行觀看:)
參考程式碼
C++
class TrieNode{
public:
string str;
int cnt;
unordered_map<char, TrieNode*> child;
TrieNode(): str(""), cnt(0){};
};
struct cmp{
bool operator() (const pair<string, int> &p1, const pair<string, int> &p2){
return p1.second < p2.second || (p1.second == p2.second && p1.first > p2.first);
}
};
class AutocompleteSystem {
public:
AutocompleteSystem(vector<string> sentences, vector<int> times) {
root = new TrieNode();
for(int i = 0; i < sentences.size(); i++){
insert(sentences[i], times[i]);
}
curNode = root;
stn = "";
}
vector<string> input(char c) {
if(c == '#'){
insert(stn, 1);
stn.clear();
curNode = root;
return {};
}
stn.push_back(c);
if(curNode && curNode->child.count(c)){
curNode = curNode->child[c];
}else{
curNode = NULL;
return {};
}
dfs(curNode);
vector<string> ret;
int n = 3;
while(n > 0 && !q.empty()){
ret.push_back(q.top().first);
q.pop();
n--;
}
while(!q.empty()) q.pop();
return ret;
}
void dfs(TrieNode* n){
if(n->str != ""){
q.push({n->str, n->cnt});
}
for(auto p : n->child){
dfs(p.second);
}
}
void insert(string s, int cnt){
TrieNode* cur = root;
for(auto c : s){
if(cur->child.count(c) == 0){
cur->child[c] = new TrieNode();
}
cur = cur->child[c];
}
cur->str = s;
cur->cnt += cnt;
}
private:
TrieNode *root, *curNode;
string stn;
priority_queue<pair<string,int>, vector<pair<string, int>>, cmp > q;
};
複製程式碼
Python
程式碼由小夥伴 Toby Qin 和 xiaodong 提供:)
class TrieNode:
def __init__(self):
self.children = dict()
self.sentences = set()
class AutocompleteSystem(object):
def __init__(self, sentences, times):
"""
:type sentences: List[str]
:type times: List[int]
"""
self.buffer = ''
self.stimes = collections.defaultdict(int)
self.trie = TrieNode()
for s, t in zip(sentences, times):
self.stimes[s] = t
self.addSentence(s)
self.tnode = self.trie
def input(self, c):
"""
:type c: str
:rtype: List[str]
"""
ans = []
if c != '#':
self.buffer += c
if self.tnode: self.tnode = self.tnode.children.get(c)
if self.tnode: ans = sorted(self.tnode.sentences, key=lambda x: (-self.stimes[x], x))[:3]
else:
self.stimes[self.buffer] += 1
self.addSentence(self.buffer)
self.buffer = ''
self.tnode = self.trie
return ans
def addSentence(self, sentence):
node = self.trie
for letter in sentence:
child = node.children.get(letter)
if child is None:
child = TrieNode()
node.children[letter] = child
node = child
child.sentences.add(sentence)
複製程式碼