KMP算法简介
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
下面开始分步讲解一下KMP算法的模式
从模式串t中提取加速匹配的信息
在KMP算法中,通过分析模式串t,从中提取出加速匹配的有用信息。这种信息是对于每一个t的每个字符t[j](0<=j<=m-1)存在一个整数k(k<j),使得模式串t中开头的k个字符依次与t[j]的前面k个字符相同,如果k有多个值,k取其中最大的一个值。模式串t中的每一个位置j的字符都有这样的信息,采用next数组表示,即是next[j]=Max(k);
下面来举一个例子
模式串t=“aaab" ,对于j=3,就是模式串中下标为3的字符t[3]=”b“,观察前面的字符串,可以得到t[2]=t[0]=“a”。
aaab 加了删除线的为t[0]- aa
ab 加了删除线的为t[2]
· 可得 k=1;
aaab 加了删除线的为t[0]t[1]- a
aab 加了删除线的为t[1]t[2]
可得k=2;
所以next[3]=Max{1.2}=2;
归纳next数组取值的规律
next的求解过程如下(图文并茂)
- next[0]=-1,next[1]=0,
- 如果next[j]=k,表示有”t[0]t[1]…t[k-1]“=“t[j-k]t[j-k+1]…t[j-1]”
1.如果t[k]=t[j], 即是”t[0]t[1]…t[k-1]t[k]“=“t[j-k]t[j-k+1]…t[j-1]t[j]”,显然有next[j+1]=k+1.
2.如果t[k]!=t[j],说明t[j]之前不存在长度为next[j]+1的字串和开头字符起的子串相同,那么是否存在一个 长度较短的子串和开头字符起的子串相同呢?设k’=next[k],则下一步是应该将t[j]与t[k’]比较:若两者相同,则说明tj之前存在长度为next[k’]+1的子串和开头字符起的子串相同;否则依次类推寻找更短的子串,直到不存在可匹配的子串,置next[j+1]=0。所以当t[k]!=t[j],置k=next[k]。
对应的代码如下:
void getnext(sqstring t,int next[])
{
int j,k;
j=0;k=-1;next[0]=-1;
while(j<t.length-1)
{
if(k==-1||t.data[j]==t.data[k])
{
j++;k++;
next[j]=k;
}
else
k=next[k];
}
}
KMP算法的模式匹配过程
求出模式串t的next数组后,你也许还不明白next数组的真正作用是什么。但是下面我会一一讲解,先总结一下,它是用来消除主串指针的回溯。下面我来举一个例子。
以目标串s=“aaaaab” 模式串t=“aaab”
第一趟匹配:从i=0,j=0,开始,不匹配处是i=3,j=3,虽然这次匹配失败了,可以得到部分匹配信息:s1s2与t1t2相同,如图所示
而模式串t中有next[3]=2,表明t1t2=t0t1,所以有s1s2=t0t1;
原来第二趟匹配是需要从i=1,j=0开始的,即需要回溯,现在既然有s1s2=t0t1,第二趟匹配可以从i=3,j=2开始,即保持主串指针i不变,模式t右滑动1(=j-next[3])个位置,让si和tnext[j]对齐进行比较。
下面来讨论一般情况。设置目标串s=“s0s1…sn-1”,模式串t=“t0t1…tm-1”,在进行第i-j+1趟时出现的不匹配情况
目标串s: s0 s1....si-j si-j+1 .. si-1 **si** si+1.....sn-1
模式串t: t0 t1.....tj-1 **tj** tj+1......tm-1
发生不匹配的为si!=tj,这时候发生部分匹配是“t0t1…tj-1”=“si-jsi-j+1…si-1”,
显然在k<j时有:tj-ktj-k+1…tj-1=si-ksi-k+1…si-1
因为next[j]=k,即:
t0t1…tk-1=tj-k tj-k+1…tj-1
由上面两个公式可得“t0t1…tk-1”=“si-ksi-k+1…si-1”,
下一趟就不在从si-j+1开始匹配,而是从si-k开始匹配,并且直接将si与tk进行比较,这样就可以把i-j+1趟比较失败时的模式串t从当前位置直接右滑动j-k个字符,如下图所示。
上述过程中,从第i-j+1趟直接转到第i-k+1趟匹配,中间可能遗漏一些匹配趟数,那么KMP算法是否对呢?实际上,因为next数组next[j]=k,容易证明中间的匹配趟数是没有必要的。
下面我们通过一个实例验证,设目标串s=“s0 s1 s2 s3 s4 s5 s6”,模式串t=“t0 t1 t2 t3 t4 t5”,next[5]=2,从s1开始匹配,不匹配处为s6!=t5,这里 i=6,j=5,k=2,下面说明第i-j+2(=3)到第i-k(=4)趟是不必要的。
如下图所示,部分匹配信息有“t1t2t3t4”=“s2s3s4s5“,因为next[5]=2,有”t0t1“=“t3t4”,同时有”t1t2t3t4“!=“t1t2t3t4”(若不相同,则next[5]=4,而不是2),从而推出”s2s3s4s5“!=“t0t1t2t3”。所以从s2开始匹配是不必要的。
同样的道理,因为next[5]=2,有“t0t1t2”!=“t2t3t4”,(若相同则next[5]=3),推出从s3开始匹配是不必要的,下一趟应该从s4开始匹配,而且是直接将s6与t2进行比较。
所以,当模式串t中t0与目标串s中某个字符si不匹配时,用next[0]=-1,表示t中已经没有字符与当前字符si进行比较了。i应该移动到目标串s的下一字符,再和模式串t中的第一个字符进行比较。
KMP算法的过程如下:
//sqstring在这里我设置的是一个结构体
int KMPIndex(sqstring s,sqstring t)
{
int i=0,j=0;
int next[MaxSize];
getnext(t,next);
while(i<s.length&&j<t.length)
{
if(j==-1||s.data[i]==t.data[j])
{
i++;
j++;
}
else
j=next[j];
}
if(j>=t.length)
return i-t.length;
else
return -1;
}
设主串s的长度为n,子串t的长度为m,KMP算法的平均时间复杂度为O(n+m)。
改进后的KMP算法
首先我们来看一下上面算法的缺陷
上述的模式串的next[4]={-1,0,0,1}; 当比较到i,j时发生不匹配,此时next[j]=1,所以应该将i保持不变,j移动到1的位置。
可以发现这里明显还是不匹配,完全没有意义,因为前一个步骤中后面的B已经不匹配了,前面一个B也是不可能匹配的,同样的情况其实还发生在第2个元素A上。
这里我们可以观察到问题的原因:t[j]==t[next[j]]
这也就是说,按照我们前面的原因得到next[j]=k,在模式串中有t[j]=t[k],当目标串中的字符s[i]与模式串中的t[j],比较不同时,s[i]!=t[k],所以没有必要再将s[i]和t[k]进行比较,而是直接将s[i]与t[next[k]]进行比较,为此这里我们把next[j]修改为nextval[j].
nextval数组的定义是nextval[0]=-1,当t[j]=t[next[j]],nextval[j]=nextval[next[j]],否则nextval[j]=next[j];
用nextval取代next,得到改进的KMP算法如下:
void Getnextval(int nextval[],String t)
{
int j=0,k=-1;
nextval[0]=-1;
while(j<t.length)
{
if(k == -1 || t[j] == t[k])
{
j++;k++;
if(t[j]!=t[k])//当两个字符相同时,就跳过
nextval[j] = k;
else
nextval[j] = nextval[k];
}
else k = nextval[k];
}
}
public int KMPIndex(String s, String t) {
int nextval[Maxsize];
int i=0,j=0;
Getnextval(t,nextval);
while(i<s.length&&j<t.length)
{
if(j==-1||s.data[i]==t.data[j])
{
i++;
j++;
}
else
j=nextval[j];
}
if(j>=t.length)
return i-t.length;
else
return -1;
}
完整c语言实现 未改进优化的
#include<stdio.h>
#include<string.h>
struct sqstring
{
char data[100];
int length;
};
int next[100];
sqstring s;
sqstring t;
void getnext(sqstring t,int next[])
{
int j,k;
j=0;k=-1;next[0]=-1;
while(j<t.length-1)
{
if(k==-1||t.data[j]==t.data[k])
{
j++;k++;
next[j]=k;
}
else
k=next[k];
}
}
int KMPIndex(sqstring s,sqstring t)
{
int i=0,j=0;
getnext(t,next);
while(i<s.length&&j<t.length)
{
if(j==-1||s.data[i]==t.data[j])
{
i++;
j++;
}
else
j=next[j];
}
if(j>=t.length)
return i-t.length;
}
int main()
{
int location;
scanf("%s",t.data);
t.length=strlen(t.data);
scanf("%s",s.data);
s.length=strlen(s.data);
location=KMPIndex(s,t);
printf("匹配字符串的首地址是:%d",location);
return 0;
}
运行截图
算法复杂度与前者一样都为O(n+m)
java实现参考KMP算法图解