对于KMP算法,刚接触可能会很迷糊,它用在哪?怎么用?
KMP的用在哪
我们应该都做过这样一类题,就是两个字符串 s1 和 s2,比较较长的字符串 s1 是否包含 字符串 s2,这个听上去很简单,不过事实也的确如此,但是如果这个字符串很长呢?
对于字符串 s1 是 “ aaaaaaaaab ”,字符串 s2 是 “ aaab ”,那么要将字符串 s2 遍历 7 遍才行,它的时间复杂度是 O(N*M)(注:N 为 s1 的长度,M 为 s2 的长度)。如果 s1 和 s2 都足够长呢?比如说字符串 s1 的前 都是 ‘ a ‘,第 +1 个是 ’ b ‘,而字符串 s2 是前 是 ‘ a ’,第 +1个是 ‘ b ’,这样的输入在使用先前的方法,肯定会时间超限。
有三个前辈,D.E.Knuth、J.H.Morris 和 V.O.Pratt 发表了一个模式匹配算法,可以大大避免重复遍历的情况,我们把它称为克努特-莫里斯-普拉特算法,简称 KMP 算法。
下面是我对 KMP 算法的理解:
先由浅入深,从朴素模式匹配算法入手:
那么对于这样两个字符串 s1:“ abcdefghijk ” 与字符串 s2:“ abcdex ”。
第一次匹配不成功:
第二次继续匹配,也不成功:
第三次匹配不成功:
第四次匹配不成功:
第五次匹配不成功:
第六次匹配不成功:
这里可以看出来,从 i=2 开始到 i=6,字符串 s1 的首字母与 ‘ a ’ 都不相等,所以其实中间的步骤 2-6 可以没必要,可以省略。
该怎么省?先再看一个例子:对于字符串 s1:“ abcabcabd ” 与字符串 s2:“abcacd ”。
第一次匹配不成功:
第二次匹配不成功:
第三次匹配不成功:
第四次匹配成功:
从朴素算法来看,s2 的首位(‘ a ’)与 s1 的 i=4 (‘ a ‘)相等,s2 的 j=2 处(’ b ’)与 s1 的 i=5 (‘ b ’)相等。那么 s2的首字符 ‘ a ’ 与第二位 ‘ b ’ 也可以不用比较了,肯定是相等的,相应的步骤二和步骤三有可以省略。也就是这样:
通过两步找到对应的片段,对比我们可以发现,朴素算法的 i 值是通过不断地回溯来完成的,而KMP 算法避免了回溯,既然 i 值不会回溯,就不会减小,就要考虑变化的 j 值。
第二个例子提到了 s2 的首字符与自身后面的字符的比较,发现如果有相同字符,j 值的变化就不会相同。也就是说 j 值的变化与主串没有什么关系,取决于 s2 中是否有重复问题。
上图中由于 i=6 与 j=6 是不匹配,而 s2 的前缀 “ ab ” 与最后的 “ ab ” 相同,所以 j 由 6 变成了 3。规律就是: j 值的大小取决于当前字符之前的串的前缀和后缀的相似的元素个数的多少。
那么在找 s1 中与 s2 相同的片段之前,可以对 s2 做一个分析,提高查找的速度,也就是找到在 s2 各个位置 j 值的变化定义为一个数组 next,那么 next 的长度就是 s2 中的长度。
next数组值的推导
当 j = 1 时,next[j]=0;
当 j ! =1 时,前缀和后缀重复的元素个数为 k,next[j]=k+1。
可能不太直观,下面是几个例子:
(1)s2= “ abcdef ”
当 j=1 时,next[1]=0;
当 j=2 时,j 由 1 到 j-1 只有字符 ‘ a ’,next[2]=1;
当 j=3 时,j 由 1 到 j-1 串是 “ ab ”,没有前后缀相等的情况,next[3]=1;
之后同理,没有相同的字母,next[j]=1.
(2) s2= " ababaaaba "
当 j=1 时,next[1]=0;
当 j=2 时,next[2]=1;
当 j=3 时,next[3]=1;
当 j=4 时,j 由 1 到 j-1 的串是 “ aba ”,前缀字符 “ a ” 与后缀字符 “ a ” 相等,next[4]=2;
当 j=5 时,j 由 1 到 j-1 的串是 “ abab ”,由于前缀字符 “ ab ” 与后缀字符 “ ab ” 相等,所以 next[5]=3;
当 j=6 时,j 由 1 到 j-1 的串是 “ ababa ”,由于前缀字符 “ aba ” 与后缀字符 “ aba ” 相等,所以 next[6]=4;
当 j=7 时,j 由 1 到 j-1 的串是 “ ababaa ”,由于前缀字符 “ a ” 与后缀字符 “ a ” 相等,所以 next[7]=2;
当 j=8 时,j 由 1 到 j-1 的串是 “ ababaaa ”,由于前缀字符 “ a ” 与后缀字符 “ a ” 相等,所以 next[8]=2;
当 j=9 时,j 由 1 到 j-1 的串是 “ ababaaab ”,由于前缀字符 “ ab ” 与后缀字符 “ ab ” 相等,所以 next[9]=3;
了解 next 数组的手算方法后,接下来通过问题来理解代码:
问题:输入两个字符串 s1 和 s2,然后利用 KMP 算法求出 s1 中含有多少个片段与 s2 相同,并且输出 相同部分的第一个字母在 s1 中的下标。
代码如下:
#include<stdio.h>
#include<string.h>
char a[100],b[100];//s1和 s2字符串
int next[100],n,m;
void fun(int pos)//记录 s1中当前的位置
{
int i=pos,j=1;
while(i<=n&&j<=m)
{
if(j==0||a[i]==b[j])
{
i++;j++;
}
else
j=next[j];
}
if(j>m)//如果找到了,就输出
{
printf("%d\n",i-m);
fun(i);//并且从当前位置继续往下找
}
return ;
}
int main()
{
int i,j;
scanf("%s",a);
scanf("%s",b);
//求出字符串的长度
n=strlen(a);
m=strlen(b);
//将字符串的下标从 0开始
for(i=n-1;i>=0;i--)
a[i+1]=a[i];
for(i=m-1;i>=0;i--)
b[i+1]=b[i];
//最后加上结束符
a[n+1]='\0';
b[m+1]='\0';
//求 next数组
i=1,j=0;
next[1]=0;//下标为 1时,next[1]=0
while(i<m)
{
if(j==0||b[i]==b[j])
next[++i]=++j;
else
j=next[j];
}
fun(1);//传入下标 1
return 0;
}
KMP模式匹配算法的改进
对于 s1:“ aaaaabaaaaax ”,s2:“ aaaaax ”。
即使使用了 next 数组来减少复杂度,对于上面这个例子,可能还是要经历 7个步骤,但是明显可以看出 s1 中的 “ b ” 在 s2 中是不存在的,所以存在多余的判断。
可以把上一个代码中的求 next 数组的部分改为 nextval 数组。
片段代码改进如下:
i=1,j=0;
nextval[1]=0;
while(i<m)
{
if(j==0||b[i]==b[j])
{
i++;
j++;
if(b[i]!=b[j])
nextval[i]=j;
else
nextval[i]=nextval[j];
}
else
j=nextval[j];
}