我接触字符串匹配算法大概有一年半了(从八年级的四月份开始),但是目前为止,这些字符串匹配算法对我来说概念还是比较模糊,特此写一个专题,总结一下。
题目
对于一个文本T,希望能找到与模式P相匹配的 T的子串T'的位置。其中,T的长度记为n,P的长度记为m,且总保证n>=m
朴素算法
其真正的时间复杂度应该为Θ((n-m+1)m)。
这种方法的好处在于易于理解,当m比较大时,此方法的优势非常大。
朴素算法的具体内容不做介绍。
Rabin-Karp算法
这种算法的时间复杂度不稳定。最差时和朴素算法一样O((n-m+1)m)。
对于长度为m的字符串s,可以建立一个映射hash(s)(hash表),得到一个值。此操作称为预处理。
比如:
// ASCII a = 97, b = 98, r = 114.
hash("abr") = (97 × 1012) + (98 × 1011) + (114 × 1010) = 999,509
参见维基百科
这样,把每一个长度为m的T的子串都进行hash(T')运算,并与hash(P)比较,若hash(T')==hash(P),则进一步比较T'与P即可。
利用有限自动机进行匹配
请参见算法导论583页,或维(伪)基(娘)百科
此算法的预处理时间为O(|∑|m),匹配时间为Θ(n)。
有限自动机的概念:
1. 有限自动机,aka DFA,aka Deterministic Finite Automaton, aka 确定有限状态自动机,可以使用一个五元组表示
其中,Q为状态集,∑为字母表,δ是转移函数(),q0 是开始状态,F是终态集(接受状态集合)
2. ∑*(其中的*)被称作Kleensche Hülle,表示由∑所产生的所有可能的字符串的集合。比如{"ab", "c"}* = {ε, "ab", "c", "abab", "abc", "cab", "cc", "ababab", "ababc", "abcab", "abcc", "cabab", "cabc", "ccab", "ccc", ...}
3. 如果有限自动机在状态q时读入了字符a,则它从状态a变为状态δ(q,a)。每当当前状态q属于F,就说自动机M接受了迄今为止读入的字符串。
没有被接受的输入称为被拒绝的输入。
4. ϕ称为终态函数,它是从∑*到Q的函数,满足ϕ(w)是M在扫描字符串w后终止的状态。因此,当且仅当ϕ(w)属于F时,M接受字符串w。
ϕ的定义为:ϕ(ε)=q0, ϕ(wa)=δ(ϕ(w),a)
字符串匹配自动机的概念:
1. 定义一个辅助函数σ,表示一个从∑*到{0,1,2,…,m}的映射。
即:σ(x)=max{k: Pk后缀于k}
例如:对于P=ab,有σ(ε)=0, σ(ccaca)=1, σ(ccab)=2
2. 字符串匹配自动机的定义如下:
a) 状态集合Q={0,1,…,m}。
b) 开始状态q0是0状态,并且只有装填m是唯一被接受的状态。
c) 对于任意的状态q和字符a,状态转移函数定义为δ(q,a)=σ(Pqa)
3. 易证:ϕ(Ti)=σ(Ti)
ねね~上面这些学会了话,预处理就很容易写出来了。

1 #include <iostream> 2 #include <cstdio> 3 #include <string> 4 #include <cstring> 5 #include <algorithm> 6 7 using namespace std; 8 const int SigmaSize=3,MAXM=1000; 9 const string Sigma="abc"; 10 string T,P; 11 int State[MAXM][SigmaSize+1]={0}; 12 void init_delta(){ 13 for (int i=0;i<=P.size();i++){ /*注意此处要写<=*/ 14 for (int j=0;j<SigmaSize;j++){ 15 string patt,temp; 16 temp=P.substr(0,i)+Sigma[j]; 17 int k=min(i+1,(int)P.size()); /*i+1的大小不能超过m*/ 18 patt=P.substr(0,i+1); 19 if (temp.size()>patt.size()) temp=temp.substr(1,temp.size()-1); 20 cout<<i<<":"<<endl<<temp<<endl<<patt<<endl; 21 while (temp!=patt&&k!=0){ 22 k--; 23 temp=temp.substr(1,temp.size()-1); 24 patt=patt.substr(0,patt.size()-1); 25 } 26 State[i][j]=k; 27 } 28 } 29 } 30 31 32 int main(){ 33 freopen("String_DFA.in","r",stdin); 34 cin>>T>>P; 35 init_delta(); 36 return 0; 37 }
然而,毕竟我技不如人,别人写的代码怎么看我都觉得惊叹不已。放上别人的代码对比一下吧。

1 bool cmp(char p[],char j,int k,int l) 2 { 3 int ret = (p[k-1]==j); 4 if( ret == 0 ) return true; 5 for(int i = 0;i<k-1;++i) 6 if(p[l-k+1+i] == p[i]) ++ret; 7 return !(ret == k); 8 } 9 int transition(char p[]) 10 { 11 int m = strlen(p); 12 for(int i=0;i<=m;++i) 13 { 14 for(int j=0;j<=255;++j)//这是精华所在啊!以为ascii中刚好256个字符所以这里代表了整个字符。 15 //所以不管什么字符都不要处理。但是缺点是占用大量的空间。 16 { 17 int k = ((m+1<i+2)?m+1:i+2); 18 int l = i; 19 do 20 { 21 k = k-1; 22 }while(k>0&&cmp(p,j,k,l)); 23 t[i][j] = k; 24 } 25 } 26 return m; 27 }

1 void fsm(char s[],char p[]) 2 { 3 int n = strlen(s); 4 int m = transition(p); 5 int q = 0; 6 for(int i = 0;i<n; ++i) 7 { 8 q = t[q][s[i]]; 9 if( q == m ) 10 { 11 printf("Pattern occur with %d ",i+1-m); 12 } 13 } 14 putchar(' '); 15 }