我们的目标是将模式串与待匹配串进行匹配,看模式串是否在待匹配的串之中,先来看传统的BF算法
1. BF算法:
模式串
a | b | a | b | a | c | b |
i |
|
|
|
|
|
|
待匹配串
a | b | a | b | a | b | a | a | b | a | b | a | c | b |
j |
|
|
|
|
|
|
|
|
|
|
|
|
|
模式串用p(n)表示,设定指针i表示当前正在进行匹配的字符;源串用T(m)表示,用指针j表示当前正在匹配的字符。于是,若p(i)=T(j),则i++,j++;对于上例,当i和j运行到红色区域时,出现了第一次不匹配的情况,也就是说若p(i)!=T(j),此时需要进行源串的回溯。那么回溯到哪个地方呢?那么由于此时已经和模式串中的已经匹配了i-1个串,所以回退i-1个字符,故此时指针的更新应该是:i=1,j=j-i+1;如此往复,直到i==n表示已经识别出来,或者j==m表示源串已经识别完毕。
对此算法的优化分析:该算法复杂度略高的原因是,每当出现一个不匹配的情况,就要回溯所有已经识别的模式串中的字符,而实际上,前面已经比较过,回溯意味着计算的重复。对于本例中的情况,显然模式串向右滑动一个,不能匹配;向右滑动两个,可以匹配到红色字符串,也就是说:若知道已经匹配的串中,最长的真前缀(这个前缀还是已经匹配 串的后缀)是什么,就能很大地简化计算:最长真前缀不用回溯重复计算。通关观察这个算法,我们发现对模式串的识别过程,已经蕴含了相当多的信息,如果直接采用BF回溯的方法,这些信息就没有有效利用,所以应该将模式串中蕴含的信息存储起来,供给下一次匹配继续使用。借助这种思想,提出KMP算法:
2. KMP算法
先看一下,为了优化上述算法,我们需要做的关键工作:
| a | b | a | b | a | c | b |
|
|
|
|
|
| i |
|
| a | b | a | b | a | b | a | a | b | a | b | a | c | b |
|
|
|
|
|
| J |
|
|
|
|
|
|
|
|
此时,p(i)与s(j)不再匹配,但是蓝色部分却是匹配的,而且这一部分只是和模式串有关系,那么如何描述这个蓝色子串的性质呢?它是:使得b1b2···bf(s)是最长的即使b1b2···bs的真前缀,又是b1b2···bs的后最的子串。因此,KMP算法有两个部分,第一是求解f(s)函数;第二是利用此函数进行模式匹配。
(1) 求解f(s)函数
失效函数的计算,失效函数是针对于模式串而言的。
给出几个例子:
1 | a | b | a | b | a | a | b | a |
0 | 0 | 1 | 2 | 3 | 1 | 2 | 3 | |
2 | a | b | a | b | a | b | b | a |
0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | |
3 | a | b | b | a | a | b | b | a |
0 | 0 | 0 | 1 | 1 | 2 | 3 | 4 | |
4 | a | b | a | b | a | c | b |
|
0 | 0 | 1 | 2 | 3 | 0 | 0 |
|
我们最先想到的往往是蛮力法:利用子串进行匹配,若成功,则停止;反之,将子串长度减一,再进行匹配;重复上述过程即可。例如,求上表中的f(5)的时候,先看abab是否满足,不满足,于是用aba,满足,于是最长就是aba,长度为3。求解f(6)的时候,同样采用蛮力试探的方法,但这个比较费时,最长算的f(6)等于1。我们当然知道,即使这样,在模式串长度很小,源串长度很大的时候依然可以减少时间开销,但是还有没有继续优化的空间呢?
注意到:在第一个串中,算得f(4)=2,此时b3=b5,所以很显然f(5)等于2+1=3;会不会f(5)>3呢?用反证法:加入f(5)>3,那么f(4)>2,与前面矛盾。也就是说,在计算f(s)的时候,如果bs=bf(s-1)+1那么,直接可以推到得到bs=bf(s-1)+1.但是如果
bs=bf(s-1)+1不成立呢?这时就相当于是计算f(6)的情况。此时,实际上我们要找的是子串的子串,而这个已经有了计算结果,所以可以找到。再比较下一个元素是否相等,相当则找到,反之,继续重复上述过程。写成程序如下:(注意下面的程序的当前变量是b【s+1】)
t=0;
f(1)=0
for(s=1;s<n,s++)
{
while(t>0&&b[s+1]!=b[t+1]) t=f(t);
if(b[s+1]==b[t+1])
{
t++;
f(s+1)=t;
}
}
(2) 模式匹配
现在已知模式串p(n)和失效函数f(n),求解是否有其中有匹配的模式串,如果有,返回模式串匹配的位置,,其中算法的关键之处在于
S=0;
For(i=0;i<m;i++)
{
While(s>0&&a[i]!=b[s+1])s=f[s];
If(a[i]==b[s+1])s++;
If(s==n) return true;
}
Return false