弄懂kmp主要是理解next数组,更重要的是k=next[k]这一行"普通"的代码
理解之后便会觉得异常简单与奇妙 “杰斯卡 莫斯卡 一起跟我来到kmp的妙妙屋吧~”
之所以出现kmp算法 是因为暴力枚举太耗时间啦
它的作用是什么呢---->查找子串
即在文本串中寻找那个模式串
例如文本串“BBQABCDABCDABE"我们要找"ABCDABE”
通常我们首先想到给他枚举出来,那就是一个一个比较直到找到他为止,我们先看一下
暴力枚举
int IfHave(char *a,char *b)
{
int la=strlen(a);int lb=strlen(b);
int i=0,j=0;
while(i<la&&j<lb)
{
if(a[i]==b[j])
{
i++,j++;
}else
{
i=i-j+1,j=0;//如果不匹配j回到0重新与文本串匹配,而文本串则去往前走一步
}
}
if(j==lb)
{
return i-j;//返回子串所在文本串的位置
}else{
return -1;//没找到返回-1
}
代码中i=i-j+1是让文本字符串向前一个字符,也就是说模式串相对于文本串往前走了一个字符的长度。
这样算的话就会有弊端,你一个一个比较 遇到不匹配的就重新比较
如果你非常“幸运”的每次走到最后一个都不匹配,并且数据很长,那你中奖了它的时间复杂度O(N x M),你将愉快的等待你的程序崩溃。
我们来用暴力法匹配一下:
BBQ ABCDAB C DABE
ABCDAB E
上述例子匹配到这里了 发现最后一个C和E不匹配 你的程序于是从下一个挨个匹配了四次,
但是聪明的你一眼就跳到下一个AB...一次就完成了匹配,你心想要是程序也能一次跳到那里该多好....
KMP引入
所以kmp超人横空出世,三个小老头发明了此种算法让我们十分受益。
这个kmp就是帮我们跳跃的。
如何完成这个跳跃呢,我们假设文本串在数组s,模式串在数组t,我们看到
BBQ ABCDAB C DABE
ABCDAB E 之所以能让它完成跳跃是因为模式串中有相同的前缀和后缀 “AB”,我们的模式串是一个个匹配到这里的,意味着目前文本串有的我模式串都有,在t[6]之前最大限度的前缀与后缀的重合长度为2,我们讲模式串向前移动几个字符长度可以使其重合?答案是4个。
不知道你们发现没有,我将数字加粗了 ,它们之间存在着某种关系,你知道牛顿莱布尼茨公式么,和这个没关系芜湖~。
我们通过简单的观察可以得到2+4=6这个震惊世界的发现
ok,渐入佳境了bro,现在t匹配到哪儿了你知道吧,在“6”这;当前这个字符前最大限度的前缀与后缀的重合长度你知道吧,看出来的,是“2”(不要慌张小阿giao,后续会讲到这个具体该怎么来,实质上他就存在next数组中),大声告诉我重新匹配的话这个模式串要往前走几步?!
对啦!6-2=4步,如何实现让模式串往前走的动作呢,这就需要在第一行说的那个“普通的”代码了。
为了方便理解引一下最开始的代码
int IfHave(char *a,char *b)
{
int la=strlen(a);int lb=strlen(b);
int i=0,j=0;
while(i<la&&j<lb)
{
if(a[i]==b[j])//这里也将会有一些改动 改为if(j==-1||a[i]==b[j])
{
i++,j++;
}else
{
在这把代码改成实现跳跃就ok了
}
}
if(j==lb)
{
return i-j;//返回子串所在文本串的位置
}else{
return -1;//没找到返回-1
}
中间小结
说到这里了,可能有同学有点小混乱,我们捋一下。
1.暴力枚举不爽,引出kmp
2.实现kmp就是需要改写原来的代码实现跳跃
3.实现跳跃有两步
(1)先找到需要跳跃几步
(2)让模式串按照这个步数跳就完事了
一、找到需要跳跃的步数----->{next数组篇}
我们前面提到的“最大限度的前缀与后缀的重合长度”这个东西,是用眼睛看出来的,我们现在要用代码实现它,来获取它。由刚才的例子我们需要清楚一点,即我们需要知道模式串中每一个当前字符以前的最大限度的前缀与后缀的重合长度。(就像刚才提到的t[6],对应的next数组next[6]=2)翻译过来就是当前t[6]这个字符以前的最大限度的前缀与后缀的重合长度为2.
"ABCDABE”我们现在来找模式串 t 对应的的 next 数组
j=0时是 A 单个字符令next[0]=-1(稍后解释为啥是-1)
j=1时是 AB 此时指向B next[1]=0
j=2时是 ABC 此时指向C next[2]=0
j=3时是 ABCD 此时指向D next[3]=0
j=4时是 ABCDA 此时指向A next[4]=0
j=5时是 ABCDAB 此时指向B 字符‘B’前有前缀后缀相同‘A’ next[5]=1
j=6时是 ABCDABE 此时指向E 字符‘E’前有前缀后缀相同‘AB’ next[6]=2
至此我们找到了数组t的next数组为:-1 0 0 0 0 1 2
但是我们一个个去遍历数组t来找next是不是也很暴力呢?
于是我们可以用递归来求
当我们知道了next[j]=k 那么求next[j+1]是怎样处理的呢,求next[j+1]就需要比较t[j+1]以前的字符串了
1.当 t [k] = t [j] 的时候就有 next [j+1] = next [j] + 1 = k + 1 。为什么要用t[k]和t[j]比较,这个t[k]怎么来的? k代表了t[j]以前的最大限度的前缀与后缀的重合长度我们直接从t[k]比较是不是省去了很多不必要的匹配呢~
例如上面的例子中:
j=5时是 ABCDAB 此时指向B 字符‘B’前有前缀后缀相同‘A’ next[5]=1
我们求next[6],判断t[5](t[5]=‘B’)是否等于t[1](t[1]=‘B’) if(t[k]==t[j]) next[j+1]=k+1
j=6时是 ABCDABE 此时指向E 字符‘E’前有前缀后缀相同‘AB’ next[6]=2
2.当t[k]!=t[j]的时候如何处理。当t[k]!=t[j]的时候我们需要从t[k]往前找到更短的相同前后缀来进行匹配,我们让k=next[k],即比较t[next[k]]与t[j],也就是新的t[k]和t[j]比较
这里引用一下大佬v_JULY_v的图来方便大家理解一下
(图片中的p数组就是本文中说的t数组)
另一个读者OnlyotDN特意在上面图的基础上又做了一些注解,供大家参考:
如果此时新的t[k]!=t[j]则我们继续让k=next[k],直到k=-1(上面有讲next[0]=-1,此时就从头开始匹配了)或者两者匹配为止。
至此 说明了如何获取模式串的next数组
附上代码
void get_next(char *t)
{
int lt=strlen(t);
int j=0,k=-1;next[0]=-1;
while(j<lt-1)
{
if(k==-1||t[j]==t[k])
{
++k,++j;
next[j]=k;
}
else
{
k=next[k];
}
}
}
赋值k的初值为-1是为了方便前缀与后缀的匹配使其保证一前一后的进行匹配。这也就是使next[0]=-1,当k=0时,往前找新的k=next[k]=-1,可以使得其从头进行匹配。
二、实现模式串的跳跃
回顾一下开头所说的,现在我们已经解决了next数组(最大限度的前缀与后缀的重合长度)
怎么跳跃呢,相对运动~
当发现失配后,我们令j=next[j]
BBQ ABCDAB C DABE
ABCDAB E
此时t[6]!=s[9] j=next[6]=2 然而此时i没动还停留在文本串‘9’的位置,而j则变成了j=2
也就是说现在比对的是s[9]与t[2],如下所示:
BBQABCD AB CDABE
AB CDABE
相当于移动了6-2=4个字符长度,所以控制模式串向前移动的关键代码就是j=next[j]这一行代码
至此,我们解决了最后一步----->模式串的跳跃!
献上KMP代码
int KMP(char *a,char *b)
{
int la=strlen(a);int lb=strlen(b);
int i=0,j=0;
while(i<la&&j<lb)
{
if(j==-1||a[i]==b[j])
{
i++,j++;
}else{
j=next[j];//模式串的跳跃
}
}
if(j==lb)
{
return i-j;//返回模式串在文本串的位置
}else{
return -1;//找不到返回-1
}
}
到这里,KMP的讲解就完毕了,当然KMP还可以进一步简单优化,感兴趣的猿可以了解,这里不做过多的赘述~
小记
这是第一次写稍长的讲解博客,也是以初学者的身份讲述,可能会有一些小错误,还望请大家斧正~
参考文献
2.《算法训练营》,陈小玉著