题目来源
https://leetcode-cn.com/problems/multi-search-lcci/
分析
题目就是让从一长串字符中,搜索最多1e5
个字符串,找到所有字符串出现的位置。
第一种朴素的做法就是直接查找,但是不能暴力查找,因为暴力搜索时间复杂度是O(n * n)
,很容易超时。可以使用KMP算法,他的时间复杂度是O(n + m)
,加上这个题也就是O(n + m) * 1e5
,也就是1e8
左右,应该是可以的。
第二种方法就是利用字典树,先对所有的子串进行建树,因为子串都是小写字母,所以可以建26个以每个小写字母为根的树,树的每个节点都有最多26个孩子节点。需要注意的是,树的叶子节点一定是一个需要找的子串,但是也可能树的中间的一个节点也是子串,所以我们需要再建立一个额外的数组来保存每个子串出现的位置。
时间复杂度 建树是 O(1e8) + 搜索 O(1e6)
也是可以完成的。
KMP思路
KMP算法就是一个跳跃式的寻找前缀的算法。
我们平常的暴力写法是这样的
这样的搜索复杂度是n * m
了,而KMP算法却可以让这个复杂度变成n + m
,因为他们都有一个公共前缀aa
,我们每次遍历基本上就可以说是线性的。
每一次我们发现对应位置的字符不相同的时候,就向前移动next数组,这个时候就有两种情况
- 前面没有与这个前缀相同的字符,那么就从当前位置再重新查找
- 前面有相同的字符,就直接从这个字符开始,向后匹配。
在这之前,我们需要维护我们的next数组,这样就可以方便我们遍历的时候更快的找到我们公共前缀
然后我们再遍历源字符串,从源字符串中查找匹配的情况。
完整代码
class Solution {
public:
vector<vector<int>> multiSearch(string big, vector<string>& smalls) {
vector<vector<int> > ans;
//依次搜索所有的子串
for(auto& eoch : smalls)
ans.push_back(kmp(big,eoch));
return ans;
}
vector<int> kmp(string s,string p)
{
vector<int> ret;
int lens = s.size();
int lenp = p.size();
//如果有一个为空的字符串,说明匹配不到结果
if(!lens || !lenp) return ret;
vector<int> next(lenp,0);
for(int i = 1, j = 0; i < lenp; i++)
{
//确保j-1合法 当前位置不相等,就跳到上一个位置
while(j && p[i] != p[j])
j = next[j-1];
if(p[i] == p[j])
j++;
next[i] = j;
}
for(int i = 0, j = 0; i < lens; i++)
{
while(j && s[i] != p[j])
j = next[j-1];
if(s[i] == p[j])
j++;
//成功匹配
if(j == lenp)
{
ret.push_back(i-lenp+1);
j = next[j-1];
}
}
return ret;
}
};
字典树
整个过程分为三步
- 先把所有待匹配的字符串,依次以他们的首字符为根节点,然后构成一棵树,并在每一次遍历完成之后,统计叶子节点的下标。因为存在,
caa
和caaa
,这样的话第二次统计就覆盖了前一次的叶子节点。 - 第二步就是搜索了,从源字符串的每一个位置开始,每一次都统计出以当前位置开始的待匹配字符串,把他们用一个hash表保存起来。
- 遍历待匹配字符串,找到他们每一个字符串出现的位置数组。
完整代码
class Solution {
public:
//字典树
const static int N = 100010;
int son[N][26];//字典树
int idx;
string key[N];
vector<vector<int>> multiSearch(string big, vector<string>& smalls) {
//生成字典树
for(auto& eoch : smalls)
insert(eoch);
//查找所有子串出现的情况
unordered_map<string,vector<int> > hash;
for(int i = 0; i < big.size(); i++)
search(big,i,hash);
vector<vector<int> > ans;
for(auto& eoch : smalls)
ans.push_back(hash[eoch]);
return ans;
}
void insert(string& s)
{
int p = 0;
for(int i = 0; i < s.size(); i++)
{
int u = s[i] - 'a';
//如果这个地方之前没有出现过这个字母的路径,就新加一个节点
if(!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
//叶子节点记录这个字串的信息
key[p] = s;
}
void search(string& s,int index,unordered_map<string,vector<int> >& hash)
{
int p = 0;
for(int i = index; i < s.size(); i++)
{
int u = s[i] - 'a';
if(!son[p][u]) return; //说明没有需要查找的子串
p = son[p][u];
//key[p] != "" 判断这条路径是不是叶子节点,是叶子节点就添加开始位置到图中
if(key[p] != "") hash[key[p]].push_back(index);
}
}
};