KMP算法可能是大家接触的第一个难以理解且高度抽象的算法了,笔者在最开始的学习里也一直都深受其困扰,难以窥见门道。但是经过一个下午的思考后,终于理解了其内涵。
这里需要推荐一篇我认为讲解的非常棒的博客,正是在它的启迪下,我对KMP才能迅速上手。
https://blog.csdn.net/weixin_46007276/article/details/104372119
一下是我个人总结的一些笔记,希望能对大家有所帮助。
KMP算法:
核心是next数组的计算
实际算法只需要在bp算法里将j回溯的位置调整为next【j】即可
理解next数组(手动写出next数组)
1.前后缀概念(看该字符前的字符串,从首尾两端开始,正序取不同数目的字符串比对,匹配上的最长字符数即为next值)
2.初始化,默认第一个元素的值为-1,第二个为0(next值)。
3.之后进行比较得到对应的next值,若完全失配取0(回溯到第一个位置),之后依次类推(配对长度为1回溯到1)。
(以上是最经典的模式,适用于下标从0开始存储字符串,且第一个元素初始化next为-1,第二个为0)
(在头歌版本,0下标不放元素,从1开始,且第一个元素next赋值为0,第二个为1,完全失配取1,与经典模式区别在于头两个元素的next赋值与失配取值)
(注意,无论是哪个版本,第一个元素都是特殊处理而非正常回溯,因为-1与0在对应位置并没有可以回溯的值,在主实现KMP的函数中会有所体现)
next函数编写
初始化k=0,j=1;(或者经典模式里k=-1,j=0开始)
第一个条件是(k=0(特殊处理)或者匹配情况下(j位置与k位置字符元素同))情况下
j,k同步后移(没失配情况下k始终指向j后面一个位置)
若不满足条件(未回溯到k=0或失配)
k=next【k】(将k再次回溯到前面,再次进行字符比对,j始终不变,不匹配层层往里回溯,若一直匹配到最后也失配则执行k=0的特殊化处理,j向后移动一位)
(注意,next【j】的赋值与j的移动仅仅在第一个条件里完成,也就是说j移动的条件是两种:1是往前回溯的过程中找到了匹配的字符,重新进入条件中完成赋值。 2.是回溯到最底层,此刻的k取特殊化处理的值(0或-1),然后重新进入,next值更新为k+1,然后j指针继续移动到下一个字符进行next值的赋值)
nextval函数(next函数的改进)
只有一小步改动:在进入第一个条件内,j,k完成后移后,需要判断一下T【j】与T【k】的值是否相同(若相等代表前后两个比较配对的字符相等,此刻的next【j】没必要更新成k(因为两个字符相同,而可以更新为next【k】),这样会节省了连续相等字符带来的重复遍历)
步骤:看T[j]与T[k]是否相等,相等,nextval【j】=nextval【k】
否则 nextval【j】=k(这一步是next求解的正常操作)
int Get_Nextval(SString &T,int nextval[])
{
int j=1,k=0;
nextval[1]=0;
while(j<T.length )
{
if(k==0 || T.ch[j]==T.ch[k])
{
j++;
k++;
if(T.ch[j]!=T.ch[k])
nextval[j]=k;
else
nextval[j]=nextval[k];
}
else
{
k=nextval[k];
}
}
}
(因为我们先移动j指针,即进行j++操作后才进行nextval【j】的赋值,所以遍历结束的条件就是j<T.length(不是<=),这一点容易出错)
int Index_KMP(SString &S,SString &T,int nextval[])
{
int i=1;
int j=0;
while(i<S.length && j<T.length)
{
if(S.ch[i]==T.ch[j] || j==0)
{
i++;
j++;
}
else
j=nextval[j];
}
if(j>=T.length)
return (i-T.length);
if(i>=S.length)
return 0;
}