在文本编辑中,经常要找出某一个模式在一段文本中全部出现的位置。这可以用字符串匹配问题来求解,不过这一章节仅考虑长度有限的字符串。如果一个模式P(长度为m)是从文本T(长度为n)中第(s+1)个字符开始出现,我们则说模式p在文本T中出现并且位移为s(0<=s<=n-m)。
本章节给出了求解字符串匹配问题的四种算法,分别是朴素算法,Rabin-Karp算法,有限自动机算法,Knuth-Morris-Praat算法。除了朴素算法外,另外三个算法都对模式P进行了一些预处理,然后找寻所有位移,我们称第二步为匹配。在这篇文章中,先介绍朴素算法和Rabin-Karp算法。
朴素算法:
朴素算法应该属于暴力搜索法,用一个循环找出所有有效位移s(0<=s<=n-m),该循环对n-m+1个可能的每一个s值检查模式P是否与文本中从第s+1个字符开始匹配。代码如下:
//判断模式字符串P是否与从文本中位置pos开始的字符串匹配(文本从位置0开始);
template<class C>
bool is_equal(const C& P,const C& T,size_t pos)
{
for(size_t i=0;i!=P.size();++i)
if(P[i]!=T[pos+i])
return false;
return true;
}
void naive_string_matcher(const string& T,const string& P)
{
size_t n=T.size();
size_t m=P.size();
if(n<m){
cout<<"the length of patter is greater than the length of text!!"<<endl;
return;
}
for(int s=0;s<=n-m;++s)
if(is_equal(P,T,s))
cout<<"pattern starts to occur from the "<<s+1<<"th character of Text"<<endl;
}
这个算法由于没有对模式P进行预处理,因此预处理时间为0,匹配时间为O((n-m+1)*m),因为对s的for循环有(n-m+1)步,is_equal函数所花费的时间为O(m)。
Rabin-Karp算法:
假设文本T和模式P的元素都是来自一个有限子母集C的字符,我们可以把C中的字符等价地转化为数字,C={0,1,2,…,d-1}(d为字母集C中元素的个数,也称做基数),依据C中字符转化数字的关系,我们相应地也把文本T和模式P中的字符也转换成了数字。
假设给定了一个素数q。给定一个由数字组成的模式P[0…(m-1)],令p表示相应的d进制值对q的模
p=[P[m-1]+d(P[m-2]+10(P[m-3]+…+10(P[1]+10P[0])…))]%q.
类似地,给定一个由数字组成的文本T[0…(n-1)],假设 ts 为长度为m的字符串T[s,s+1…s+m-1]所对应的d进制值对q的模。当p和 ts 不等时,那么P[0…(m-1)]一定与T[s,s+1…s+m-1]不匹配;如果两者相等,则可能匹配也可能不匹配,这是我们可以通过直接验证P[0…(m-1)]==T[s,s+1…s+m-1]是否成立。
我们可以用求p相同的方法来求
t0
,但当我们求
t1,t2,...,tn−m
时,我们可以用如下关系式来求解:
ts+1=(d(ts−T[s]∗(dm−1 mod q))+T[s+m]) mod q
。
有一个细节需要主要的是,我们应该要确保
d(ts−T[s]∗(dm−1 mod q))+T[s+m]
这一项应大于0,如果小于0则要相应地转换成大于0的形式。
Rabin-Karp算法代码如下:
//to calculate a^b%c;
unsigned long mod(unsigned long a, unsigned long b, unsigned long c)
{
unsigned long ret=1;
unsigned long tmp=a%c;
while(b!=0){
if(b%2!=0)
ret=(ret*tmp)%c;
b/=2;
tmp=(tmp*tmp)%c;
}
return ret;
}
void Rabbin_Karp_matcher(const vector<unsigned long>& T,const vector<unsigned long>& P,unsigned long d,unsigned long q)
{
size_t n=T.size();
size_t m=P.size();
// to calculate d^(m-1)%q;
unsigned long h=mod(d,m-1,q);
//to calculate T[0...(m-1)]%q,P[0...(m-1)]%q;
unsigned long p=0;
unsigned long t=0;
for(size_t i=0;i!=m;++i)
{
p=(d*p+P[i])%q;
t=(d*t+T[i])%q;
}
// to find the match of P in T;
for(size_t s=0;s<=n-m;++s)
{
if(p==t)
if(is_equal(P,T,s))
cout<<"pattern starts to occur from the "<<s+1<<"th character of Text"<<endl;
if(s<n-m){
long tmp=d*(t-T[s]*h)+T[s+m];
//if tmp<0, the result of tmp%q is machine independent.The case should be avoided.
if(tmp<0){
tmp=-tmp;
int k=0;
while(++k){
if(tmp<k*q){
tmp=k*q-tmp;
break;
}
}
}
t=tmp%q;
}
}
}
Rabin-Karp算法计算p和 t0 的预处理时间为 Θ(m) 。计算 t1,t2,...,tn−m 分别花费常数时间,在最坏情况下,n-m+1个s所对应的偏移中的每一个都是有效的,则验证所花费的时间为 Θ((n−m+1)m) 。因此这个算法预处理时间为 Θ(m) ,匹配时间为 Θ((n−m+1)m) 。