KMP算法
串的模式匹配即子串定位是一种重要的串操作。设S和T是给定的两个串,在主串S中找子串T的过程称为模式匹配。
一、BF算法
暴风(Brute Force)算法是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。 —— [ 百度百科 ]
S(主串):
S1S2...Sm
T(模式串):
t1t2...tn
1.1. 思路
第一轮中, S1 与 t1 对齐,比较 S1 与 t1 。若 S1 与 t1 相同,则比较 S2 与 t2 ,以此类推。
当 Si 与 tj 比较时(第一轮中i=j), Si 不等于 tj ,则重新开始下一轮比较,即 S2 与 t1 对齐,进行下一轮比较。
在这个过程中,如果T中的字符全部比较完,则匹配成功,成功的这一轮中,匹配起始位置为 i-j+1。
1.2. 准备
主串S与模式串T的0号单元存放串长,即s[0]=m,t[0]=n
从1号单元开始存放串值,即s[i]= si , t[j]= tj
1.3. 算法描述
int Index_BF(char *s, char *t)
{
int i=1, j=1; /*i为S串下标,j为T串下标*/
while(i<=s[0] && j<=t[0])
if(s[i]==t[j]) {
i++;
j++;
}
else { /*回溯,本次S中起始位置为i-j+1,故下一次为i-j+2*/
i=i-j+2;
j=1;
}
if(j>t[0]) /*匹配成功,返回起始位置*/
return i-j+1; /*(i-1)-(j-1)+1=i-j+1*/
return -1;
}
1.4. 时间复杂度分析
1.4.1 最佳情况:
T串每次都在第一个字符处匹配失败,如S=”aaaaaaaaabc”,T=”bc”。假设在 Si 处匹配成功,则前面共进行了i-1轮失败匹配,且每轮仅比较一次,最后一次成功匹配比较了n次,故总共比较了i-1+n次。
现假定从任一起始位置(i=1,2,…,m-n+1)匹配成功都是等可能的,概率是
Pi = 1m−n+1
平均比较次数是
1m−n+1∑m−n+1i=1(i−1+n)=m+n2
时间复杂度是O(m+n)
1.4.2 最坏情况:
T串每次都在最后一个字符处匹配失败,如S=”aaaaaaaaaab”,T=”ab”。假设在 Si 处匹配成功,则前面共进行了i-1轮失败匹配,且每轮比较次数为n,最后一次成功匹配比较了n次,故总共比较了i*n次,在等概率条件下,平均比较次数是
1m−n+1∑m−n+1i=1(in)=n(m−n+2)2
时间复杂度为O(m*n)
1.5 结论
BF算法简单但效率较低,进行许多无效比较,故算法不够理想。
1.6举例
主串:abababc
模式串:abac
第一轮比较到第四个字符失配,第二轮从第一个字符开始失配,为无效的一轮,因为第一轮已经得到前三个字符为aba,所以可以跳过第二轮,直接进入第三轮匹配。
二、KMP算法
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。 —— [ 百度百科 ]
造成BF算法速度慢的原因是回溯,即在某一轮失配后,对于S串要回到本轮开始字符的下一个字符,T串要回到第一个字符,而这些回溯并不是必要的。如果在某一轮 si 与 tj 失配后,指针i不回溯,模式串T向右“移动”到某个位置上,使 tk 与 si 对准,继续向右进行匹配,那么算法效率会得到提高。
模式串: t1t2...tn
2.1 分析示例
example 1: S=”abababac”, t=”abac”
t的前三位 t1t2t3 与 sisi+1si+2 匹配成功,即 sisi+1si+2 =”aba”,所以下一轮中,可以直接从 si+2 开始匹配,因为我们知道从 si+1 开始匹配必然是无效的。
example 2: S=”abcabcabcb”, t=”abcabcb”
t中 t1t2t3t4t5t6 与 sisi+1si+2si+3si+4si+5 匹配成功,即 sisi+1si+2si+3si+4si+5 =”abcabc”,所以在下一轮中,可以直接从 si+3 开始匹配,因为显然 si...si+5 中的后缀abc可以作为下一轮中与 t1t2t3 匹配的abc,所以下一轮直接从 si+3 匹配,从 si、si+1、si+2 开始显然是无效的。
由上述两例分析可知,我们是根据上一轮中的 sisi+1...si+j−2 来判断下一轮中应从哪一个位置开始匹配,显然 sisi+1...si+j−2 和 t1t2...tj−1 相同,所以 t1t2...tj−1 决定下一轮模式串应该移动多少位。
回到example1和example2中,子串 t1t2...tj−1 分别为”aba”与”abcab”。不难发现,我们是先找到其相同的最大前缀和后缀,”aba”中为a,”abcab”中为ab,然后模式串移动至其前缀与 sisi+1...si+j−2 中相同的最大后缀对应的位置即可开始下一轮匹配。
2.2 准备
2.2.1 Substr(S, m, n)子串截取函数
T串中存在下列等式:
Substr(T, 1, k-1) = Substr(T, j-k+1, j-1) ——描述示例中的最大前缀与最大后缀
模式串T中每一个 ti 对应一个k值,且k值仅依赖于模式串T本身,与主串S无关。
2.2.2 Next()函数
现用next[j]表示
tj
对应的k值,含义是当
tj
匹配失败时,使第k个字符代替其进行匹配。
定义:next[j] =
2.3 思路
Next()函数求值问题可以看成一个模式匹配问题,整个模式串既是主串又是模式。
若 tj=tk (最大前缀与最大后缀同时增加一个 tk 和 tj ),则next[j+1]=next[j]+1;
若 tj≠tk ,则Substr(T, 1, k) ≠ Substr(T, j-k+1, j),此时,应将模式向右滑动,next[k] = k1,使第k1个字符与主串中的第j个字符作比较 (这样做的原因是将其看成一个局部的KMP算法问题,主串和模式串均为T串)
若 tk1=tj ,则next[j+1]=next[k]+1,即Substr(T, 1, k1)=Substr(T, j-k1+1, k1);
若 tk1≠tj ,则继续将模式向右移动,next[k1] = k2,依次类推,直至匹配成功;或者不存在任何kn使得Substr(T, 1, kn)=Substr(T, j-kn+1, j),此时,若 t1≠tj+1 则next[j+1]=1,即从这一步开始重新用 t1 来匹配,主串不移动,否则next[j+1]=0,下一步重新开始用 t1 匹配,主串移动了一位。
2.4 算法描述
先看Index_KMP(),再看getNext()。
int Index_KMP(char *s, char *t)
{
int i=1, j=1;
while(i<=s[0] && j<=t[0])
if(s[i]==t[j] || j==0){ /*匹配成功或者直接从下一位重新开始从T串第一位匹配*/
i++;
j++;
}
else
j=next[j]; /*回溯,从第k个字符开始下一轮,如果k=0则会从下一位重新开始*/
if(j>t[0])
return i-j+1;
return -1;
}
void getNext(char *t, int next[])
/*求取模式串T中每个单元的next值,存入next[]数组中*/
{
int i=1, j=0;
next[1]=0;
while(i<t[0]){
while(j>0 && t[i]!=t[j]) /*不断尝试匹配,直至找到可以成功匹配的第j位字符*/
j=next[j]; /*跳出循环有两种情况,情况一是匹配成功,情况二是j=0*/
i++; /*主串移动一位*/
j++; /*模式移动一位*/
if(t[i]==t[j])
next[i]=next[j];
else
next[i]=j;
}
}