745困难. 前缀和后缀搜索
题意
设计一个包含一些单词的特殊词典,并能够通过前缀和后缀来检索单词。
实现 WordFilter 类:
WordFilter(string[] words) 使用词典中的单词 words 初始化对象。 f(string pref, string suff) 返回词典中具有前缀 prefix 和后缀 suff 的单词的下标。如果存在不止一个满足要求的下标,返回其中 最大的下标 。如果不存在这样的单词,返回 -1 。
示例 1:
输入 ["WordFilter", "f"] [[["apple"]], ["a", "e"]]
输出 [null, 0] 解释 WordFilter wordFilter = new WordFilter(["apple"]); wordFilter.f("a", "e"); // 返回 0 ,因为下标为 0 的单词:前缀 prefix = "a" 且 后缀 suff = "e" 。
提示
:
- 1 <= words.length <= 104
- 1 <= words[i].length <= 7
- 1 <= pref.length, suff.length <= 7
- words[i]、pref 和 suff 仅由小写英文字母组成
- 最多对函数 f 执行 104 次调用
相关标签
字符串 、字典树、设计
辅助理解:
- Take "apple" as an example, we will insert add "apple{apple", "pple{apple", "ple{apple", "le{apple", "e{apple", "{apple" into the Trie Tree.
- If the query is: prefix = "app", suffix = "le", we can find it by querying our trie for "le { app".
- We use '{' because in ASCii Table, '{' is next to 'z', so we just need to create new TrieNode[27] instead of 26. Also, compared with traditional Trie, we add the attribute weight in class TrieNode. You can still choose any different character.
AC代码
👀方法一:Java版本
class WordFilter {
Trie root;
Trie toor;
public WordFilter(String[] words) {
root = new Trie(); //作为根
toor = new Trie(); //作为反向字符串的根
int t=0;
for(String word:words) add(root,word,t++,true);
t=0;
for(String word:words) add(toor,word,t++,false);
}
public int f(String pref, String suff) {
Trie tmp=root;
// 先找前缀,之后再找后缀的第一开始的字符
char[] chs = pref.toCharArray();
for(char c:chs){
if(tmp.next[c-'a']==null) return -1;
else tmp = tmp.next[c-'a'];
}
Set<Integer> list = new HashSet<>();//记录前缀所含有的索引
//注意是否存在前缀或者后缀为空的情况
// list.addAll(tmp.flag);
List<Integer>l1 = tmp.flag;
chs = suff.toCharArray();
int len = chs.length;
Trie toor1 = toor;
for(int i=len-1;i>=0;i--){
if(toor1.next[chs[i]-'a']==null) return -1;
else toor1 = toor1.next[chs[i]-'a'];
}
List<Integer> l2 = toor1.flag;
int n = l1.size();
int m = l2.size();
for (int i = n - 1, j = m - 1; i >= 0 && j >= 0; ) {
if (l1.get(i) > l2.get(j)) i--;
else if (l1.get(i) < l2.get(j)) j--;
else return l1.get(i);
}
// Set<Integer> list1 = new HashSet<>(); //这么干超时
// list1.addAll(toor1.flag);
// for(int t:list1){
// if(list.contains(t)){
// max1 = Math.max(max1,t);
// }
// }
return -1;
}
public void add(Trie root ,String word,int t,boolean flag){
char[] chs = word.toCharArray();
if(!flag){
int len = chs.length;
char c;
for(int i=0;i<len/2;i++){
c = chs[i];
chs[i]=chs[len-i-1];
chs[len-1-i]=c;
}
}
for(char c:chs){
if(root.next[c-'a']==null){
root.next[c-'a'] = new Trie();
}
root = root.next[c-'a'];
root.flag.add(t);
}
}
}
class Trie{
List<Integer> flag= new ArrayList<>();;
Trie[] next = new Trie[26];
}
/**
* Your WordFilter object will be instantiated and called as such:
* WordFilter obj = new WordFilter(words);
* int param_1 = obj.f(pref,suff);
*/
👀方法二:Java版本
class WordFilter {
String words[];
public WordFilter(String[] words) {
this.words=words;
}
public int f(String pref, String suff) {
for(int i=words.length-1;i>=0;i--){if(words[i].startsWith(pref)&&words[i].endsWith(suff)){return i;}}
return -1;
}
}
分析
方法一
建立两颗字典树,一个字典树记录字符串的正向数结构,另一个数记录所有字符串的反向数结构,这是初始化阶段,查找阶段主要是根据前缀查找到含有该前缀的所有字符串的下标,根据后缀的反转在反向字典树中找到以指定后缀结束的字符串的下标,最后一起从后向前遍历两个list,找到第一个相等的下标就是答案,因为下标加入树节点的时候是按照从小到大的顺序加入的,最需要注意的就是这个遍历的过程,如果去正向的一个个遍历,最后是超市的状态,所以这个需要注意。
方法二
直接暴力比较每一个字符串是不是含有在指定的前缀和指定的后缀,相比较字典树,这个暴力反而很好的时间复杂度和空间复杂度。
题解过程分析
方法一
- root = new Trie(); //作为根
- toor = new Trie(); //作为反向字符串的树根
- 之后初始化,需要注意的是Trie中有一个flag,记录的是每一个经过这个点的前缀,
- add方法是把初始化的时候给的每一个字符串正向插入到root和反向插入到toor中
- f方法中主要是先正向查找前缀字符串的所有下标,再在toor中查到后缀反转后的查询下标。之后分别从后向前遍历两个list,找到第一个相等的就是答案。
方法二:
纯纯的暴力,不讲就能看懂的那种,遍历字符串数组,直接使用startwith和endwith判断字符串是不是以指定的前缀和后缀结尾的。
复杂度分析
最后看了看三叶姐的思路和我的一样,我就把他的时间和空间复杂度搬过来了,自己就不分析了。
-
时间复杂度:初始化操作复杂度为O(∑i=0n−1ss[i].length),检索过程复杂度为O(a+b+n),其中a=b=7 为前后缀的最大长度,n = 1e4 为初始化数组长度,代表最多有 n 个候选下标,
-
空间复杂度:O(\sum_{i = 0}^{n - 1} ss[i].length)O(∑i=0n−1ss[i].length)
总结
今天的总结除了上边的做题思路就一句话,草泥马在奔腾,我真是服了超时卡在遍历对比上开了一上午,万脸蒙蔽的我无可奈何。