题目:给一个字符串S和一个字符串数组T(T中的字符串要比S短许多),设计一个算法,在字符串S中查找T中的字符串。 思路:
字符串的多模式匹配问题。
我们把S称为目标串,T中的字符串称为模式串。设目标串S的长度为m,模式串的平均长度为 n,共有k个模式串。如果我们用KMP算法(或BM算法)去处理每个模式串,判断模式串是否在目标串中出现,匹配一个模式串和目标串的时间为O(m+n),所以总时间复杂度为:O(k(m+n))。一般实际应用中,目标串往往是一段文本,一篇文章,甚至是一个基因库,而模式串则是一些较短的字符串,也就是m一般要远大于n。这时候如果我们要匹配的模式串非常多(即k非常大),那么我们使用上述算法就会非常慢。这也是为什么KMP或BM一般只用于单模式匹配,而不用于多模式匹配。
那么有哪些算法可以解决多模式匹配问题呢?貌似还挺多的,Trie树,AC自动机,WM算法,后缀树等等。我们先从简单的Trie树入手来解决这个问题。
Trie树,又称为字典树,单词查找树或前缀树,是一种用于快速检索的多叉树结构。比如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树。
回到我们的题目,现在要在字符串S中查找T中的字符串是否出现(或查找它们出现的位置),这要怎么和Trie扯上关系呢?
我们发现,如果一个串t是S的子串,那么t一定是S某个后缀的前缀。比如t = bc,那么它是后缀bcd的前缀;又比如说t = c,那么它是后缀cd的前缀。
因此,我们只需要将字符串S的所有后缀构成一棵Trie树(后缀Trie),然后查询模式串是否在该Trie树中出现即可。如果模式串t的长度为n,那么我们从根结点向下匹配,可以用O(n)的时间得出t是否为S的子串。
代码:
#include <iostream>
#include <cstring>
using namespace std;
class Trie{
public:
static const int MAX_N = 100 * 100;// 100为主串长度
static const int CLD_NUM = 26; // 每个结点的儿子数量(26个字母)
int size; // 用到的当前结点编号
int trie[MAX_N][CLD_NUM];
Trie(const char* s);
void insert(const char* s);
bool find(const char* s);
};
Trie::Trie(const char* s){
memset(trie[0], -1, sizeof(trie[0]));
size = 1;
while(*s){
insert(s);
++s;
}
}
void Trie::insert(const char* s){
int p = 0;
while(*s){
int i = *s - 'a';
if(-1 == trie[p][i]){
memset(trie[size], -1, sizeof(trie[size]));
trie[p][i] = size++;
}
p = trie[p][i];
++s;
}
}
bool Trie::find(const char* s){
int p = 0;
while(*s){
int i = *s - 'a';
if(-1 == trie[p][i])
return false;
p = trie[p][i];
++s;
}
return true;
}
int main(){
Trie tree("mississippi");
string patt[] = {
"is", "sip", "hi", "sis", "mississippa"
};
int n = 5;
for(int i=0; i<n; ++i)
cout<<tree.find((char*)&patt[i][0])<<endl;
return 0;
}
后缀Trie的查找效率很优秀,如果你要查找一个长度为n的字符串,只需要O(n)的时间,比较次数就是字符串的长度,相当给力。但是,构造字符串S的后缀Trie却需要O(m2 )的时间, (m为S的长度),及O(m2 )的空间。