Given an array of strings products
and a string searchWord
. We want to design a system that suggests at most three product names from products
after each character of searchWord
is typed. Suggested products should have common prefix with the searchWord. If there are more than three products with a common prefix return the three lexicographically minimums products.
Return list of lists of the suggested products
after each character of searchWord
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 no repeated 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/