大家好,我是A导,一名在校大学生。最近自学数据结构,被弄的死去活来,功夫不负有心人,经过不断地学习,终于弄懂了一点东西。本着服务同学,让大家少走弯路的态度,我决定分享一些数据结构学习的心得和一些算法的通俗解释和代码,供大家交流参考。
这个系列的博客我准备坚持写下去,也算是对自己大学学习的一点记录。我尽量做到一周一篇文章的更新(毕竟我也要学习(打游戏))。欢迎各位同学前来交流学习。最后,本人水平有限,如有错误,欢迎指正。
感谢这位大佬给我的启发,我参考并转载他的一些图片@大佬的博客链接
开篇文章偏长,但讲解部分绝对没有一句废话(除了讲解部分就是我随便bb的,可跳过(^ _ ^),请耐心的看到最后,反复阅读,相信你会有收获的!
首先我们先贴上一张导图,大概介绍下本文的结构和讲解点
如果采用暴力破解(BF算法),每次字符串匹配失败后,指针都会回溯到一开始的地方,再次进行匹配,该算法最坏情况下要进行M*(N-M+1)次比较,时间复杂度为O(M*N),这是一种很慢的方式。而kmp算法巧妙地通过一个Next数组(这里后面会讲到)减少了在字符串比较过程中不必要的回溯,从而节省时间。
二:什么是kmp算法(枯燥的理论知识这里就不作论述了,请各位自行百度什么是kmp算法)
我们先贴一个动图,对第一点kmp算法优越性进行描述,大家也可以正好了解下kmp算法的运作过程(下面会进行一些详细的阐述)
图片链接(侵删)
从上面的gif图片我们可以直观的看出kmp算法在匹配失败后,不会回溯到字串的第一个字符,而是回到子字符串的某个位置进行下次比较。
我们先上一下我祖传的kmp算法代码,大家可以先对算法进行一个总体的了解(看不懂没关系,后面会一一分析)可以看完之后自己手动在纸上画一下代码运行的步骤加深理解
int KMP(int start,char S[],char T[])/*这里我们默认数组第一个位置
也就是S[0]和T[0]存的是数组的长度*/
{
int i=start,j=0;
int next[255];
get_next(T,next);//get_next数组在下文中会给出代码和介绍
while(S[i]!='\0'&&T[j]!='\0')
{
if(j==0||S[i]==T[j])
{
i++; //继续对下一个字符比较
j++; //模式串向右滑动
}
else
j=next[j];
}
if(T[j]=='\0')
return (i-T[0]); //匹配成功返回下标
else
return -1; //匹配失败返回-1
}
//没找到
从第一点的介绍我们可以了解到,kmp算法的精髓,就是他的一个Next数组,这个数组标记了在匹配失败之后,指针回溯的位置。
那么问题来了1.什么是next数组?
首先我们来解释一个名词:最长公共前后缀。假设有一个串P=“p0p1p2 …pj-1pj”。如果存在p0p1…pk-1pk = pj-kpj-k+1…pj-1pj,我们就说在P串中有一个最大长度为k+1的公共前后缀。
那么这里又有了一个问题
2.如何寻找前后缀?、
找前缀时,要找除了最后一个字符的所有子串。
找后缀时,要找除了第一个字符的所有子串。
这里可能还有些同学没明白是什么意思,我们给上一张图来说明
现在有子串P=abaabca,前后缀如图所示
这里我们给出Next数组的求值规则
(图片来源:《大话数据结构》)
然后我们可以写出字符串p所对应的Next数组
可能有的同学看到这个next数组还是一脸懵逼,那我们就来手动的推一下
1.j=1(j是指向字符串p的指针)根据上面的规则此时Next[1]=0
2.j=2,p[j]=b,向前找公共字符串,此时前面只有一个元素a,属于上述第三种情况。此时Next[2]=1
3.j=3,向前找字串,没有公共子串,属于上述第三种情况,此时Next[3]=1
4.j=4,向前找字串,发现有公共子串 aba 其中a为公共子串,长度为1,所以Next[4]=1+1(如果有子串,那next的值为子串长度+1),此时Next[4]=2
5.j=5,向前找字串,发现有公共子串 abaa 其中a为公共子串,长度为1,此时Next[5]=2
6.j=6,向前找字串,发现有公共子串 abaab 其中a为公共子串,长度为1,所以Next[4]=1+1(如果有子串,那next的值为子串长度+1),此时Next[6]=3
7.j=7,向前找字串,发现没有公共子串 ,属于上述第三种情况,此时Next[7]=1
下面给出几个Next数组的实例供大家练习
下面我们讨论如何用机器语言实现求Next数组
三. Next数组的代码实现void GetNext(char T[],int *next)
{
int i=1,j=0;
next[1]=0;
while(T[i]!='\0')
{
if(j==0||T[i]==T[j])
{
++j;
++k;
next[i]=j;
}
else
j=next[j];
}
}
建议各位同学找一个字符串在草稿纸上按步骤“运行”一次代码。
运行完代码,大家觉得最神奇的应该就是j=next[j]的这一步回溯,可能不禁产生疑问:为什么匹配失败后往前进行j=Next[j]的回溯就可以找到长度更短的相同前缀后缀呢?
下面我们来解决这个问题
下面我们看这张图
在pk != pj时,k = next[k],用pnext[k]去跟pj继续匹配。为什么不用其他值和pj匹配呢?我们可以看到,在pj之前,有一段长度为k的已匹配串;在pk之前有一段蓝色的已匹配串,说明pk字符前有一段长度为next[k]的最大公共前后缀(蓝色的那段)。如果pk != pj,说明p0p1…pk-1pk != pj-kpj-k+1…pj-1pj,那么我们只能找更短的最大公共前后缀,此时因为pk和pnext[k]前面的蓝色串已完全匹配,如果pnext[k]能和pj匹配 ,那么我们便找到了我们需要的串(就是p0到pnext[k]那段长度)。如果还是不匹配,下一步pnext[next[k]…]去跟pj继续匹配,直到找到长度更短公共前后缀。
到这里,我们关于Next数组的内容就结束了。
我们来看一张图
其中字符串aaaax对应的next数组如下图
我们看上图的匹配过程,发现其实2,3,4,5这几步都可以省略。这就是Next输出的不足,在某些字符串的比较中,会出现重复的没有必要的比较。所以,我们引入Next_val数组。
下面给出Next数组的推导规则:当Next[j]所对应的字符=Next[Next[j]]所对应的字符时,则Next[j]=Next[Next[j]],依次递推,直到Next[j]所对应的字符!=Next[Next[j]]所对应的字符时,结束。
我们用上述字符串的Next数组来推导Next_val数组:
1.Next[1]=0 Next_val[1]=0.
2.Next[2]=1,Next[1]=a,Next[2]=a,。此时Next_val[2]=Next[1]=0
3.Next[3]=2,Next[2]=a,Next[3]=a此时Next[3]=Next[2]=1然后,Next[1]=a,Next[2]=a,所以Next_val[3]=Next[1]=0
4.Next[4]=3,同理可得Next_val[4]=0
5.Next[5]=4同理可得Next_val[5]=0
6.Next[6]=5,Next[5]=a,Next[6]=x, a!=x,所以Next_val[6]=5
我们得出Next_val数组
这个推导过程光看文字可能有点难理解,要多多自己动手推导,我们这里依然给出几个实例供大家参考
最后,我们给出Next_val推导的代码(和我们手动推导的过程差别很大),需要大家自己找个例子手动在纸上过一遍代码的过程比如上面第一张图的Next_val我们可以用代码推导一遍。
void GetNext_val(char T[],int *nextval)
{
int i=1,j=0;
nextval[1]=0;
while(T[i]!='\0')
{
if(j==0||T[i]==T[j])
{
++j;
++k;
if(T[i]!=T[j])
nextval[i]=j;
else
nextval[i]=nextval[j];
}
else
j=nextval[j];
}
}
五:应试技巧
在这里贴一个大佬的博客链接,他的方法可以很快求出next数组和nexttval数组 KMP快速计算next与nextval
六:结尾经过四晚的努力,总算完成了我真正意义上第一篇形式丰富的博客(以前的都比较枯燥),在这里感谢上述两位大佬给我的启发和一些技术支持,并感谢《大话数据结构》给我的一些代码规范启发。最后,如果你觉得本篇博客给你一些启发(如果有同班同学,欢迎来我宿舍投喂零食),可以点赞+关注支持(害羞)。后续我也会再出一些关于数据结构的教程。最后,谢谢各位能看完本文。谢谢!