学到KMP算法是不是感觉明明不难原理都懂但是总感觉没有理解透彻?next数组怎么求出来的?尤其是代码实现里的那句j=next[j]
怎么理解? 如果你脑袋里也有这些问号,不要着急,你不是一个人。耐心看完这篇文章,相信能让你茅塞顿开,豁然开朗。
一、介绍
1-1 低效的暴力匹配算法
说起字符串匹配,最直观的也是最容易想到的办法就是暴力匹配算法,也叫朴素的模式匹配算法。简单地说,就是使模式串T依次与主串S的每一位字符依次进行匹配,直至找到与模式串完全匹配的或者主串S循环完毕。如下图:
其中,第1步到第2步,比较指针从6变为1的过程我们称为回溯。
对于模式串T我们是可以方便地知道各个位置的字符的关系,例如上面的例子,我们已知,模式串Tabcaba
中T[1]
、T[2]
、T[3]
互相不相等,T[1]==T[4]
,T[2]==T[5]
。
接着我们分析第一步。由于是匹配到第6位才失败的,所以在主串S和模式串T中的前5位字符是完全一样的。
也就是
S[1]==T[1]
、S[2]==T[2]、S[3]==T[3]
、S[4]=T[4]
、S[5]=T[5]
。由于T[1]!=T[2]
和S[2]==T[2]
,所以T[1]!=S[2]
不用比较可以知道。也就是第2步的回溯是没有意义的。同理可得第3、5、6的回溯是没有意义的。
就是这些无意义的回溯的过程使得暴力匹配算法比较低效。
二、KMP算法
2-1 前置概念
- 前缀:从开头字符开始的一段连续子串,不包括最后一个字符,比如
ababa
的前缀有a
、ab
、aba
、abab
- 后缀:从任意字符开始到最后一个字符的连续子串,不包括开头字符,比如
ababa
的后缀有a
、ba
、aba
、baba
- 最长公共前后缀:就是前缀和后缀交集中最长的串,比如串
ababa
的前后缀交集是{'a','aba'}
,所以最长公共前后缀就是aba
2-2 原理
KMP算法的原理就是利用已有的匹配信息,让无意义的比较指针回溯不发生。
分析上图左边,我们可以得出两个信息,第一,主串S与模式串T的前5位字符完全相等,即
S[1,5]==T[1,5]
,第二,模式串Tabcab
有一个最长公共前后缀ab
;然后我们就可以直接把模式串T的前缀ab
移到后缀ab
的位置上,如上图右,并且比较指针不回溯,也就是模式串T直接从T[3]
开始比较。
再大胆一点,可以发现其实模式串T移动的位置只与它本身有关,跟主串S完全无关。
如上图,没有主串我们也能正确移动模式串。
那这样移动到底会不会漏掉前缀和后缀中间能够匹配的位置呢?
答案是不会,因为如果匹配上了,那说明我们找的公共前后缀
ab
并不是最长的。
如上图,最长公共前后缀就应该是红框里的字符串。
2-3 next数组到底是什么
由2-1的最后一个问题可以引出KMP算法的核心:用一个数组保存模式串中各个位置的最长公共前后缀的长度,以便在该位置不匹配时,可以快速地找到下一个该匹配的位置。这个数组被叫做next
数组,也叫作失效数组。当模式串匹配到第j位不匹配时,就让模式串直接回到位置next[j]
上,与主串的当前位置进行匹配。
2-4 如何计算next数组
根据最长公共前后缀计算next数组
当在第1个位置不匹配时,使T[1]
与主串下一位比较,我们表示为0;
当在第2个位置不匹配时,使T[1]
与主串当前位置比较,表示为1;
当在第3个位置不匹配时,由于前面已匹配的字符串ab
没有公共前后缀,也就是长度为0,使T[1]
与主串当前位置比较,表示为1;
当在第4个位置不匹配时,由于前面已匹配的字符串abc
没有公共前后缀,也就是长度为0,,使T[1]
与主串当前位置比较,表示为1;
当在第5个位置不匹配时,由于前面已匹配的字符串abca
公共前后缀a
的长度为1,,使T[2]
与主串当前位置比较,表示为2;
当在第6个位置不匹配时,由于前面已匹配的字符串abcab
没有公共前后缀,也就是长度为0,,使T[1]
与主串当前位置比较,表示为1;
最大公共前后缀与next值关系如下
我们可以推导,next值是最大公共前缀长度+1。
根据代码递推计算next数组
当在第j位不匹配,j移动到next[j]的位置上,与当前位置进行匹配,也就是继续比较pnext[j]。如下图上。但是我们发现pnext[j]上的d
与pj上的c
并不相等。于是j继续移动到pnext[j]的next值上,也就是pnext[next[j]]。将pnext[next[j]]与pj比较,b
与c
不相等,于是继续找pnext[next[j]]的next值,此时的next值为1,于是使P1与Pj比较,依然不相等。但是此时已经回溯到模式串的第一个字符了,于是我们将比较位后移
所以
j=next[j]
实际上就是使最大公共前后缀的前缀移动到后缀的位置上,如果还不相等就一直找最大公共前后缀的最大公共前后缀,直到找到相等的或者回到第一个字符为止
用代码表示如下
/*通过计算返回子串T的next数组*/
void get_next (String T, int *next)
{
int i,j;
i = 1;
j = 0;
next[1] = 0;
while (i<T[0]) /*T(0)表示串T的长度*/
{
if(j==0 || T[i] == T[j]) /*T[i]表示后缀单个字符,T[j]表示前缀单个字符*/
{
++i;
++j;
next[i]=j;
}
else{
j = next[j];/*若前后缀字符不相同,则j值回溯*/
}
}
}