字符串匹配
一. 定义
在日常生活中,我们常常需要在文本中找到某个模式的所有出现位置,比如输入字符串,需要找到出现这个字符串的所有位置,解决这个问题的算法叫做字符串匹配算法。
字符串匹配问题的形式化定义:假设文本是一个长度为n的数组T[1...n],而模式是一个床都为m的数组P[1...m],其中m<=n,进一步假设P和T的元素都是来自一个有限字母集Q的字符,如Q={0,1},或者Q={a,b...z},字符数组P和T称为字符串。如果0<=s<=n-m,并且T[s+1...s+m]=P[1...m],那么称模式P在文本T中出现,且偏移为s。字符串匹配问题就是找到所有的偏移s,使得在该偏移下,所给的模式P出现在给定的文本T中
二. 朴素字符串匹配算法
朴素字符串匹配算法即暴力枚举算法,对于每个偏移s,判断其是否匹配,得到结论。上代码
C++代码
#include<iostream>
#include<vector>
#include<string>
using namespace std;
bool match(string &T,string &P,int s,int m){
for(int i=0;i<m;i++)
{
if(T[i+s]!=P[i])
return false;
}
return true;
}
int naive_string_matcher(string T,string P){
int n=T.size();
int m=P.size();
for(int s=0;s<=n-m;s++)
{
if(match(T,P,s,m))
return s;
}
return -1;
}
int main(){
string T;
string P;
cin>>T;
cin>>P;
int s;
s=naive_string_matcher(T,P);
if(s>=0)
cout<<s;
else cout<<"NOT FOUND";
}
在naive_string_matcher函数中有一个for循环,循环n-m+1次,循环内部调用的match函数循环了m次,故在检索中的复杂度为O(m(n-m+1))。这种暴力枚举没有对字符串有任何预处理,而是直接搜素,由于检索复杂度高,并不是一个最好的方法。
三. Rabin-Karp算法
Rabin-Karp算法的预处理时间是O(m),并且在最坏的情况下,它的运行时间为O(m(n-m+1))。基于一些假设,在平均情况下,它的运行时间还是比较好的。
Rabin-Karp算法用到了一些数论的知识,为了不失一般性,假设Q={a,b,...,z},这样所有字符都是a-z之间,如果将其减去‘a',得到0-25的数值,于是字符串就可以当做是d=26进制的连续数字。给定模式P[1...m],假设p是其相应的26进制数字,假设ts表示长度为m的T[s+1...s+m]所对应的26进制数字,其中s=0,1...n-m。当且仅当T[s+1...s+m]=P[1...m]时,ts=p。
为了计算p和t0,我们可以运用霍纳法则,时间复杂度为O(m)。
为了计算剩余的值t1,t2,...,t(n-m),我们可以通过递推公式,在常数时间根据ts计算t(s+1)。
而d^m-1又可以通过反复平方法在O(lgm)的时间内完成,因此可以在O(m)内计算p,在O(n-m+1)计算t0..t(n-m)。于是可以用O(m)的处理时间,和O(n-m+1)的匹配时间内找到所有模式P[1..m]在文本T[1..n]中出现的位置。
然而,如果m数值过大,我们看到中间运算会出现一个以m指数增长的d^(m-1)这么一个大数,于是p和ts的值会大得使得我们无法处理,为了解决这个问题,我们使用哈希映射的方式,选取一个合适的模q来计算p和ts的模,于是过程匹配过程转变为,在O(m)的时间内计算出模q的p值,在O(n-m+1)时间内计算模q的所有ts值,如果选取q为一个素数,使得dq刚好在一个字长以内,那么可以在float中完成所有运算,递归式转变为
实际上就是将这些“大数“用它对q取模的数来替代。而基于模q的结果并不完美,存在这哈希冲突,ts mod q==p mod q不能说明ts==q,但如果ts mod q!=p mod q,则说明ts!=q,可作为一种启发式的方式排除错误的匹配,在满足等号的情况下需要进一步进行一次检测T[s+1...s+m]=P[1...m],如果q足够大,那么这种冲突就越少。
C++代码
#include<iostream>
#include<vector>
#include<stdlib.h>
#include<string.h>
using namespace std;
bool match(char* &T,char* &P,int s,int m){
for(int i=0;i<m;i++)
{
if(T[i+s]!=P[i])
return false;
}
return true;
}
int exponentiation(int a,int b,int n){
int d=1;
while(b>0)
{
if(b&1) /*最低位为1*/
{
d=(d*a)%n;
}
b=b>>1;
a*=a;
}
return d;
}
int Rabin_Karp_matcher(char* T,char* P,int d,int q){
int n=strlen(T);
int m=strlen(P);
int *text=(int*)malloc(n*sizeof(int));
int *pattern=(int*)malloc(m*sizeof(int));
int *t=(int*)malloc((n-m+1)*sizeof(int));
for(int i=0;i<n;i++)
text[i]=T[i]-'a';
for(int i=0;i<m;i++)
pattern[i]=P[i]-'a';
int h=exponentiation(d,m,q);/*反复平方法求数的幂的模*/
int p=0;
t[0]=0;
for(int i=0;i<m;i++)
{
p=(d*p+pattern[i])%q;
t[0]=(d*t[0]+text[i])%q;
}
for(int s=0;s<=n-m;s++)
{
if(p==t[s])
{
if(match(T,P,s,m)) /*启发式策略,当等式满足再匹配检查*/
{
free(text);
free(pattern);
free(t);
return s;
}
}
else if(s<n-m)
t[s+1]=(d*t[s]-h*text[s]+text[s+m])%q; /*递推公式,注:T以1开始,text以0开始*/
}
free(text);
free(pattern);
free(t);
return -1;
}
int main(){
char T[100];
char P[100];
cin.getline(T,100);
cin.getline(P,100);
int s;
s=Rabin_Karp_matcher(T,P,26,101); /*随便选取一个素数,这里取101*/
if(s>=0)
cout<<s;
else cout<<"NOT FOUND";
}
可以看到在这种启发式检查中,当q越大,它冲突的几率就越小,可以估计两个随机数对q取模相等的概率是1/q,于是Rabin-Karp算法的期望运行时间为O(n)+O(m(v+n/q)),v是实际偏移量。