LeetCodeAnimation/notes/LeetCode第642号问题:设计一个搜索自动完成系统.md

207 lines
7.7 KiB
Java
Raw Normal View History

2019-05-02 15:59:01 +08:00
# LeetCode 642 号问题设计一个搜索自动完成系统
> 本文首发于公众号五分钟学算法[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 642 号问题设计一个搜索自动完成系统题目难度为 Hard目前通过率为 37.8%
### 题目描述
为搜索引擎设计一个搜索自动完成系统用户可以输入一个句子(至少一个单词并以一个特殊的字符'#'结尾)对于除'#'之外的每个字符您需要返回与已输入的句子部分前缀相同的前3个历史热门句子具体规则如下:
一个句子的热度定义为用户输入完全相同句子的次数
返回的前3个热门句子应该按照热门程度排序(第一个是最热的)如果几个句子的热度相同则需要使用ascii代码顺序(先显示较小的一个)
如果少于3个热门句子那么就尽可能多地返回
当输入是一个特殊字符时它意味着句子结束在这种情况下您需要返回一个空列表
您的工作是实现以下功能:
构造函数:
AutocompleteSystem(String[] sentence, int[] times):这是构造函数输入是历史数据句子是由之前输入的句子组成的字符串数组Times是输入一个句子的相应次数您的系统应该记录这些历史数据
现在用户想要输入一个新句子下面的函数将提供用户类型的下一个字符:
List<String> input(char c):输入c是用户输入的下一个字符字符只能是小写字母(az)空格()或特殊字符(#)另外前面输入的句子应该记录在系统中输出将是前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其中ironmani love leetcode有着相同的热度既然 ASCII码为32rASCII码为114那么i love leetcode应该在ironman前面此外我们只需要输出前3个热门句子所以ironman将被忽略
操作:输入(' ')
输出:[i love youi love leetcode]
解释:
只有两个句子有前缀i
操作:输入(' a ')
输出:[]
解释:
没有以i a为前缀的句子
操作:输入(#)
输出:[]
解释:
用户完成输入后在系统中将句子i a保存为历史句下面的输入将被计算为新的搜索
注意:
输入的句子总是以字母开头#结尾两个单词之间只有一个空格
要搜索的完整句子不会超过100个包括历史数据在内的每句话的长度不会超过100句
在编写测试用例时即使是字符输入也请使用双引号而不是单引号
请记住重置在AutocompleteSystem类中声明的类变量因为静态/类变量是跨多个测试用例持久化的详情请点击这里
### 题目解析
设计一个搜索自动补全系统它需要包含如下两个方法
#### 构造方法
AutocompleteSystem(String[] sentences, int[] times): 输入句子sentences及其出现次数times
#### 输入方法
List<String> input(char c): 输入字符c可以是26个小写英文字母也可以是空格'#'结尾返回输入字符前缀对应频率最高的至多3个句子频率相等时按字典序排列
### 思路解析
核心点Trie字典树
利用字典树记录所有出现过的句子集合利用字典保存每个句子出现的次数
#### 解题思路
题目的要求是补全的句子是按之前出现的频率排列的高频率的出现在最上面如果频率相同就按字母顺序来显示
频率 这种要求很容易想到 优先队列Map等知识点这里涉及到 字典 那肯定使用 字典树 能解决
所以首先构造 Trie trieNode 结构以及 insert 方法构造完 trieNode 类后再构造一个树的根节点
由于每次都要输入一个字符我们可以用一个私有的 NodecurNode 来追踪当前的节点
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** 大佬提供动画技术支持笔芯
https://v.qq.com/x/page/m08267nr4fv.html
### 代码实现
#### 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;
};
```
![](https://bucket-1257126549.cos.ap-guangzhou.myqcloud.com/blog/fz0rq.png)