[LeetCode] 1268. Search Suggestions System 搜尋推薦系統
Given an array of stringsproducts
and a stringsearchWord
. We want to design a system that suggests at most three product names fromproducts
after each character ofsearchWord
is typed. Suggested products should have common prefix with the searchWord. If there aremore than three products with a common prefixreturn the three lexicographically minimums products.
Returnlist of listsof the suggestedproducts
after each character ofsearchWord
is typed.
Example 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"]
Example 2:
Input: products = ["havana"], searchWord = "havana"
Output: [["havana"],["havana"],["havana"],["havana"],["havana"],["havana"]]
Example 3:
Input: products = ["bags","baggage","banner","box","cloths"], searchWord = "bags" Output: [["baggage","bags","banner"],["baggage","bags","banner"],["baggage","bags"],["bags"]]
Example 4:
Input: products = ["havana"], searchWord = "tatiana"
Output: [[],[],[],[],[],[],[]]
Constraints:
1 <= products.length <= 1000
- There are norepeated elements in
products
. 1 <= Σ products[i].length <= 2 * 10^4
- All characters of
products[i]
are lower-case English letters. 1 <= searchWord.length <= 1000
- All characters of
searchWord
are lower-case English letters.
這道題讓做一個簡單的推薦系統,給了一個產品字串陣列 products,還有一個搜尋單詞 searchWord,當每敲擊一個字元的時候,返回和此時已輸入的字串具有相同的字首的單詞,並按照字母順序排列,最多返回三個單詞。這種推薦功能想必大家都不陌生,在谷歌搜尋的時候,敲擊字元的時候,也會自動出現推薦的單詞,當然谷歌的推薦系統肯定更加複雜了,這裡是需要實現一個很簡單的系統。題目中說了返回的三個推薦的單詞需要按照字母順序排列,而給定的 products 可能是亂序的,可以先給 products 排個序,這樣也方便找字首,而且還可使用二分搜尋法來提高搜尋的效率。思路是根據已經輸入的字串,在排序後數組裡用 lower_bound 來查詢第一個不小於目標字串的單詞,這樣就可以找到第一個具有相同字首的單詞(若存在的話),當然也有可能找到的單詞並不是相同字首的,這時需要判斷一下,若不是字首,則就不是推薦的單詞。所以在找到的位置開始,遍歷三個單詞,判斷若是字首的話,則加到 out 陣列中,否則就 break 掉迴圈。然後把 out 陣列加到結果 res 中,注意這裡做二分搜尋的起始位置是不停變換的,是上一次二分搜尋查詢到的位置,這樣可以提高搜尋效率,參見程式碼如下:
解法一:
class Solution {
public:
vector<vector<string>> suggestedProducts(vector<string>& products, string searchWord) {
vector<vector<string>> res;
sort(products.begin(), products.end());
string query;
auto it = products.begin();
for (char c : searchWord) {
query += c;
vector<string> out;
it = lower_bound(it, products.end(), query);
for (int i = 0; i < 3 && it + i != products.end(); ++i) {
string word = *(it + i);
if (word.substr(0, query.size()) != query) break;
out.push_back(word);
}
res.push_back(out);
}
return res;
}
};
其實這裡也可以不用二分搜尋法,還是要先給 products 陣列排個序,這裡維護一個 suggested 陣列,初始化時直接拷貝 products 陣列,然後在敲入每個字元的時候,新建一個 filtered 陣列,此時遍歷 suggested 陣列,若單詞對應位置的字元是敲入的字元的話,將該單詞加入 filtered 陣列,這樣的話 fitlered 陣列的前三個單詞就是推薦的單詞,取出來組成陣列並加入結果 res 中,然後把 suggested 陣列更新為 filtered 陣列,這個操作就縮小了下一次查詢的範圍,跟上面的二分搜尋法改變起始位置有異曲同工之妙,參見程式碼如下:
解法二:
class Solution {
public:
vector<vector<string>> suggestedProducts(vector<string>& products, string searchWord) {
vector<vector<string>> res;
sort(products.begin(), products.end());
vector<string> suggested = products;
for (int i = 0; i < searchWord.size(); ++i) {
vector<string> filtered, out;
for (string word : suggested) {
if (i < word.size() && searchWord[i] == word[i]) {
filtered.push_back(word);
}
}
for (int j = 0; j < 3 && j < filtered.size(); ++j) {
out.push_back(filtered[j]);
}
res.push_back(out);
suggested = filtered;
}
return res;
}
};
再來看一種使用雙指標來做的方法,還是要先給 products 陣列排個序,然後用兩個指標 left 和 right 來分別指向陣列的起始和結束位置,對於每個輸入的字元,儘可能的縮小 left 和 right 之間的距離。先用一個 while 迴圈來右移 left,迴圈條件是 left 小於等於 right,且 products[left] 單詞的長度小於等於i(說明無法成為字首)或者 products[left][i] 小於當前輸入的字元(同樣無法成為字首),此時 left 自增1。同理,用一個 while 迴圈來左移 right,迴圈條件是 left 小於等於 right,且 products[right] 單詞的長度小於等於i(說明無法成為字首)或者 products[right][i] 大於當前輸入的字元(同樣無法成為字首),此時 right 自減1。當 left 和 right 的位置確定了之後,從 left 開始按順序取三個單詞,也可能中間範圍內並沒有足夠的單詞可以取,所以變數j的範圍從 left 取到 min(left+3, right+1)
,參見程式碼如下:
解法三:
class Solution {
public:
vector<vector<string>> suggestedProducts(vector<string>& products, string searchWord) {
vector<vector<string>> res;
int n = products.size(), left = 0, right = n - 1;
sort(products.begin(), products.end());
for (int i = 0; i < searchWord.size(); ++i) {
while (left <= right && (i >= products[left].size() || products[left][i] < searchWord[i])) ++left;
while (left <= right && (i >= products[right].size() || products[right][i] > searchWord[i])) --right;
res.push_back({});
for (int j = left; j < min(left + 3, right + 1); ++j) {
res.back().push_back(products[j]);
}
}
return res;
}
};
當博主剛拿到這道題時,其實用的第一個方法是字首樹 Prefix Tree (or Trie),因為這題就是玩字首的,LeetCode 中也有一道專門考察字首樹的題目 Implement Trie (Prefix Tree)。首先要來定義字首樹結點 Trie,一般是有兩個成員變數,一個判定當前結點是否是一個單詞的結尾位置的布林型變數 isWord,但這裡由於需要知道整個單詞,所以可以換成一個字串變數 word,若當前位置是單詞的結尾位置時,將整個單詞存到 word 中,否則就就為空。另一個變數則是雷打不動的 next 結點陣列指標,大小為 26,代表了 26 個小寫字母,也有人會用變數名 child,都可以,沒啥太大的區別。字首樹結點定義好了,就要先建立字首樹,新建一個根結點 root,然後遍歷 products 陣列,對於每一個 word,用一個 node 指標指向根結點 root,然後遍歷 word 的每個字元,若 node->next 中該字元對應的位置為空,則在該位置新建一個結點,然後將 node 移動到該字元對應位置的結點,遍歷完了 word 的所有字元之後,將 node->word 賦值為 word。
建立好了字首樹之後,就要開始搜尋了,將 node 指標重新指回根結點 root,然後開始遍歷 searchWord 中的字元,由於每敲擊一個字元,都要推薦單詞,所以新建一個單詞陣列 out,由於根結點不代表任何字元,所以需要去到當前字元對應位置的結點,不能直接取,要先對 node 進行判空,只有 node 結點存在時,才能取其 next 指標,不然若 next 指標中對應字元的結點不存在時,此時 node 就更新為空指標了,下次迴圈到這裡直接再取 next 的時候就會報錯,所以需要提前的判空操作。有了當前字元對應位置的結點後,就要取三個單詞出來,呼叫一個遞迴函式,在遞迴函式中,判斷若 node 為空,或者 out 陣列長度大於等於3時返回。否則再判斷,若 node->word 不為空,則說明是單詞的結尾位置,將 node->word 加入 out 中,然後從a遍歷到z,若對應位置的結點存在,則對該結點呼叫遞迴函式即可,最終把最多三個的推薦單詞儲存在了 out 陣列中,將其加入結果 res 中即可,參見程式碼如下:
解法四:
class Solution {
public:
vector<vector<string>> suggestedProducts(vector<string>& products, string searchWord) {
vector<vector<string>> res;
for (string word : products) {
TrieNode *node = root;
for (char c : word) {
if (!node->next[c - 'a']) {
node->next[c - 'a'] = new TrieNode();
}
node = node->next[c - 'a'];
}
node->word = word;
}
TrieNode *node = root;
for (char c : searchWord) {
vector<string> out;
if (node) {
node = node->next[c - 'a'];
findSuggestions(node, out);
}
res.push_back(out);
}
return res;
}
private:
struct TrieNode {
string word;
TrieNode *next[26];
};
TrieNode *root = new TrieNode();
void findSuggestions(TrieNode *node, vector<string>& out) {
if (!node || out.size() >= 3) return;
if (!node->word.empty()) out.push_back(node->word);
for (char c = 'a'; c <= 'z'; ++c) {
if (node->next[c - 'a']) findSuggestions(node->next[c - 'a'], out);
}
}
};
其實並不需要遞迴函式來查詢推薦單詞,我們可以在字首樹結點上做一些修改,使其查詢推薦單詞更為高效。前面強調過 next 指標是字首樹的核心,這個必須要有,另一個變數可以根據需求來改變,這裡用一個 suggestions 陣列來表示以當前位置為結尾的字首的推薦單詞陣列,可以發現這個完全就是本題要求的東西,當前綴樹生成了之後,直接就可以根據字首來取出推薦單詞陣列,相當於把上面解法中的查詢步驟也融合到了生成樹的步驟裡。接下來看建立字首樹的過程,還是遍歷 products 陣列,對於每一個 word,用一個 node 指標指向根結點 root,然後遍歷 word 的每個字元,若 node->next 中該字元對應的位置為空,則在該位置新建一個結點,然後將 node 移動到該字元對應位置的結點。
接下來的步驟就和上面的解法有區別了:將當前單詞加到 node->suggestions 中,然後給 node->suggestions 排個序,同時檢測一下 node->suggestions 的大小,若超過3個了,則移除末尾的單詞。想想為什麼可以這樣做,因為字首樹的生成就是根據單詞的每個字首來生成,那麼該單詞一定是每一個字首的推薦單詞(當然或許不是前三個推薦詞,所以需要排序和取前三個的操作)。這樣操作下來之後,每個字首都會有不超過三個的推薦單詞,在搜尋過程中就非常方便了,將 node 指標重新指回根結點 root,遍歷 searchWord 中的每個字元,若 node 不為空,去到 node->next 中當前字元對應位置的結點,若該結點不為空,則將 node->suggestions 加入結果 res,否則將空陣列加入結果 res 即可,參見程式碼如下:
解法五:
class Solution {
public:
vector<vector<string>> suggestedProducts(vector<string>& products, string searchWord) {
vector<vector<string>> res;
for (string word : products) {
TrieNode *node = root;
for (char c : word) {
if (!node->next[c - 'a']) {
node->next[c - 'a'] = new TrieNode();
}
node = node->next[c - 'a'];
node->suggestions.push_back(word);
sort(node->suggestions.begin(), node->suggestions.end());
if (node->suggestions.size() > 3) node->suggestions.pop_back();
}
}
TrieNode *node = root;
for (char c : searchWord) {
if (node) {
node = node->next[c - 'a'];
}
res.push_back(node ? node->suggestions : vector<string>());
}
return res;
}
private:
struct TrieNode {
TrieNode *next[26];
vector<string> suggestions;
};
TrieNode *root = new TrieNode();
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/1268
類似題目:
參考資料:
https://leetcode.com/problems/search-suggestions-system/
LeetCode All in One 題目講解彙總(持續更新中...)
微信打賞 |
Venmo 打賞 |