[LeetCode] 1268. Search Suggestions System 搜索推荐系统


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


类似题目:

Implement Trie (Prefix Tree)


参考资料:

https://leetcode.com/problems/search-suggestions-system/

https://leetcode.com/problems/search-suggestions-system/discuss/1242823/C%2B%2BPython-3-solutions-Clean-and-Concise

https://leetcode.com/problems/search-suggestions-system/discuss/436674/C%2B%2BJavaPython-Sort-and-Binary-Search-the-Prefix

https://leetcode.com/problems/search-suggestions-system/discuss/510681/Java-super-easy-and-clean-solution-Beats-95-time-and-100-space

https://leetcode.com/problems/search-suggestions-system/discuss/436151/JavaPython-3-Simple-Trie-and-Binary-Search-codes-w-comment-and-brief-analysis.


LeetCode All in One 题目讲解汇总(持续更新中...)

posted @ 2021-12-12 16:44  Grandyang  阅读(811)  评论(0编辑  收藏  举报
Fork me on GitHub