引入
BF算法:
这是最容易想到的算法:模式串和主串从第一个开始分别比较,如果遇到不相同的,在主串比较的起点变成上一次比较的起点的下一个,模式串的比较起点从头开始,类似于下图:
于是我们就想啊,每次都扫描这么多,但是确因为有一个是错的而需要从头开始努力,之前的努力都白费了吗?我们要吸取之前失败的经验,进行下一次的努力,失败是成功之母嘛,因而我们开始学习一个新的算法KMP算法。
KMP算法
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
简单来说,KMP算法利用了之前浪费的东西,但是就整个算法而言,他也不仅仅是比BF算法多了利用,他有着自己的技巧和魅力。
简述
如上图:
如果是BF算法主串的箭头就会回溯到图上主串第二个位置,模式串的箭头要回溯到模式串第一个位置且模式串会往后平移一个格子,这样之前扫描过的就浪费掉了。通过观察可以发现,会出现以下情况:
失配点前面某长度的一段可能和从开头往下读同样长度的一段相同,上图就是:这两段是一样的,(我们称这样的片段为回文)因此我们不妨就让主串的箭头不再回溯,而是直接平移子串,让开头与尾部相同的位置进行匹配,继续往下比较。如下图:
这里主串的箭头并未回溯,只是模式串的箭头回溯,找到了新的比较点。
那么一定会有疑惑,一下子把模式串平移这么多,中间会不会错过一些可以匹配到的情况呢?比如会不会发生万一仅仅平移一个就匹配上了,却因为一下子平移这么多而错过了的情况呢?答案是不会的,因为一旦出现这种情况,在找回文的过程中会被找到。如下图所示:
失配点前面就两个字符,回文部分只能是第一个和倒数第一个,模式串只能让第二个A和当前主串上的箭头部分进行匹配:
因此KMP的主要魅力就是找回文部分,而且必须是最大回文部分,有的回文部分只有一个,有的甚至从头和从尾部看的两个部分有重合的地方,但是回文部分的长度不能超过失配点前面的总长度:
找到回文部分后,在模式串上就可以将回文的前部分(即图中红框)的相邻下一个位置与当前主串的失配点往下进行匹配。
当遇到失配点时:你发现不管是模式串还是主串失配点前面的都应该已经匹配到了,也就是说前面的是一样的,因此我们仅需要对模式串进行操作,因为每个点都可能失配,所以要找到每个点前面的回文部分,即找到每个点失配后的下一个用来与当前主串位置比较点。这就是KMP算法中著名的next[]数组。
我们可能会考虑以下两种特殊情况,这里给出解决办法:(这里的模式串从[1]位置开始存数据)
- 如果前面没有回文序列怎么办——直接让第一个字符的位置与当前位置比较,即next[j]=1;
- 如果第一个位置就不匹配怎么办——模式串平移一位,主串的箭头位置后移一位,从下一个开始进行比较next[1]=0(这里需要根据代码进行叙述)
下面给出笔算next[]数组的方法:
- next[1]=0,next[2]=1;
- 之后从每一个数据前面找回文部分,next[j]等于回文前半部分的下一个数据的位置;
- 若无回文next[j]=1;
例:
不难看出B、C、A前都没有回文,C前回文前半部分最后一个字符为第一个,因此next=2.
放个复杂点的例子:
可以自己试着用笔算一下next的结果。
下面将要讲的是最难理解的部分,如何用代码实现求next数组
代码求next
void get_next(char T[], int next[] )//这里T[0]存放的是数组的长度
{ //求模式串T的next函数值并存入数组next[]。
i=1;
j=0;
next[1]=0; //可能会有读者疑惑为什么在这里没有让next[2]等于1,下面的核心代码有讲到
while(i<T[0] ) //T[0]为数组长度
{
if(j== 0||T[i]==T[j])
{++i; ++j; next[i]=j;}
else j=next[j];
}
}
核心代码:
while(i<T[0] ){
if(j==0||T[i]==T[j])/*第一次进入循环时,因为j==0为正确所以进入if*/
{++i; ++j; next[i]=j;}/*i和j都加一,此时i=2,j=1因而next[2]=1*/
else j=next[j];
再次进入循环,此时j!=0,如果T[2]==T[1]并且i++,j++之后仍一直相等的话next[i]就一直等于j,类似下图情况:
前面就会是一个是一个顺子,根据求回文的方式也很容易理解。
j=next[j]
这是一句比较难理解的代码,假如从第3个数(即j=1,i=2)的时候发现,T[2]!=T[1],此时j=next[j]=next[1]=0,此时j=0,进入if ,则next[3]=1。再进行判断T[3]是否等于T[1],如果等于则T[4]=2,不等于则j=0,T[4]=1。接下来从T[3]=T[1]来说,T[4]=2后判断T[4]是否等于T[2],若不等于则j=next[2]=1,判断T[4]是否等于T[1]…相信读到这里的读者一定有了感觉,while代码很巧妙的利用了每一次的比较,看似没有进行类似T[j-n]和T[1]这样的比较,实际上在每次i、j递增的时候确实进行了比较来找到回文序列,而 j=next[j]更是巧妙地在每次匹配失败后,都可以轻松找到次长前缀字符串的最后一个字符与该字符进行比较。
KMP算法主程序
int index_KMP(char *S, char* T, int pos)
{
int i=pos, j=1;
while ( i<=S[0] && j<=T[0] ) {
if (j= =0|| S[i] = = T[j] ) {++i, ++j} //不失配则继续比较后续字符
else {j=next[j];} //特点:S的i指针不回溯,而且从T的k位置开始匹配
}
if(j>T[0]) return i-T[0]; //子串结束,说明匹配成功
else return0;
}//
KMP算法的改进
需要改进的原因就是上面的那个例子:
此时效率不高的原因为:模式串前9位相同时,主串字符若与其中一个不相等,则不必再与其余8个比较。而实际上还在依次比较。
修正next数组
i=2; nextval[1]=0; j=0;
while(i<T[0] )
{
j=next[i];
if(T[i]!=T[j])
nextval[i]=j;
else
nextval[i]=nextval[j];
i++;
//代码易读就不作注释了
手算求nextval:
建议参考这位up主的视频,会很清楚,(手算求next也可以参考)
KMP算法求next,nextval,字符匹配次数(以王道数据结构课后习题为例)
以上纯属小白的学习笔记,觉得有用的话可以参考,欢迎指出错误之处。