文章目录
前言
提示:这里可以添加本文要记录的大概内容:
记录KMP算法学习思路:
之后用到的主串S和子串T中的S[0]和T[0]存储的是该字符串的长度,S[1]和T[1]存储的才是当前字符串的第一个元素。
提示:以下是本篇文章正文内容,下面案例可供参考
一、朴素的模式匹配算法(BF暴力匹配算法)
有一主串S、子串T,返回在S中和T完全相同时的index。在朴素的模式匹配算法(BF暴力匹配算法)中是:简单来说就是:从主串s 和子串t 的第一个字符开始,将两字符串的字符一一比对,如果出现某个字符不匹配,主串回溯到第二个字符,子串回溯到第一个字符再进行一一比对。如果出现某个字符不匹配,主串回溯到第三个字符,子串回溯到第一个字符再进行一一比对…一直到子串字符全部匹配成功。
图片来看更容易理解:
竖直线表示相等,闪电线表示不等
第一个过程:子串“goo”部分与主串相等,'g’不等,结束比对,进行回溯。
第二个过程:开始时就不匹配,直接回溯
第三个过程:开始时即不匹配,直接回溯
第四个过程:开始时即不匹配,直接回溯
第五个过程:匹配成功
这种算法在最好情况下时间复杂度为O(n)。即子串的n个字符正好等于主串的前n个字符,而最坏的情况下时间复杂度为O(m*n)。
二、KMP算法
搞清楚KMP算法前,先用图片搞清楚KMP算法好在哪里:
在此之前插入一个概念:
前缀:包含第一个字符,但是不包含最后一个字符
后缀:包含最后一个字符,但是不包含第一个字符
现在我们先看一个图:第一个长条代表主串,第二个长条代表子串。红色部分代表两串中已匹配的部分,
绿色和蓝色部分分别代表主串和子串中不匹配的字符。
再具体一些:这个图代表主串"abcabeabcabcmn"和子串"abcabcmn"。
第一张图:
现在发现了不匹配的地方,根据KMP的思想我们要将子串向后移动,现在解决要移动多少的问题。
之前提到的最长相等前后缀的概念有用处了。因为红色部分也会有最长相等前后缀。
第二张图:
灰色部分就是红色部分字符串的最长相等前后缀,我们子串移动的结果就是让子串的红色部分最长相等前缀和主串红色部分最长相等后缀对齐。
第三张图:
子串T移动到图示位置,接下来只要从子串T第三个字符©和主串S(e)开始判断。此时,第一次移位结束。后面的我们暂时不考虑。
事实上,每一个字符前的字符串都有最长相等前后缀,而且最长相等前后缀的长度是我们移位的关键,所以我们单独用一个next数组存储子串的最长相等前后缀的长度。而且next数组的数值只与子串本身有关。
1.next[]数组求法
next[]数组是我们子串移位的依据,也是学习KMP算法的核心。
next[]数组却决于当前字符前的字符串前后缀的相似度。
现在有子串P=abaabca,前后缀如图所示:
next[j]数组公式如图所示:
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 其中ab为公共子串,长度为2,所以Next[4]=2` +1(如果有子串,那next的值为子串长度+1),此时Next[6]=3
7.j=7,向前找字串,发现没有公共子串 ,属于上述第三种情况,此时Next[7]=1
总结:next[j]数组的值就是当前该字符前的字符串最长相等的前后缀长度+1。
2.next[j]数组代码分析
在上面我们已经知道next[]数组的求法,但是该怎么用代码来求出next[]数组呢?
在看代码之前,我们多写几个字符串,自己用简单的最长前后缀+1来求next[]数组,在熟悉之后再用公式求。
公式中:
发现:我们只看公式中Max…部分。假设①公式成立,那么②公式一定成立,而②公式是不是我们next[j-1]的求值公式。next[j-1]=k-1。
求next[j-1]是不是只要求:
是不是有递归那味儿了!
当我们知道next[j-1]的值时,②公式一定成立。那么根据公式①,我们只要判断公式④是否相等:如果相等,则next[j]就等于next[j-1]+1。
上面是根据公式来分析写代码的思想。如果看不懂多看几遍。下面是通过核心思想来理解:
定义:next[]数组的求法就是判断某处字符前的字符串的前后缀的相似度。
next[]=最长前后缀长度+1
我们还是可以看出是递归的思想:我们想知道字符串下标为j位置的next[j],我们是不是要判断j之前的字符串的相似度,也就是下标为1…j-1字符串的相似度;要知道下标为1…j-1字符串的相似度,我们只要知道下标为1…j-2字符串的相似度,知道了下标为1…j-2字符串的相似度后,假设下标为1…j-2字符串的相似度为h,也就是说前h个字符和后h个字符相等,我们只要判断下标为j-1位置的字符和下标为h+1位置的字符相等否?如果相等,则下标为1…j-1字符串的相似度就是h+1。那么用递归再合适不过了。我们知道第一个字符的next[],再用第二个字符和第一个字符比,若相等则第二个字符的next[]值就是next[1]+1…以此类推,得到next[j-1]后就可以用j-1位置的字符和next[j-1]位置的字符进行比较,如若相等,则next[j]=next[j]+1。
以上都是j-1位置的字符和next[j-1]位置的字符相等,那如果不相等呢?
不相等的话,next[j]一定不比next[j]+1大,那我们就要在小的里面找一个前后缀较为最长的。我们找多大呢呢?这又是一个难点!
由上方定义可知:
我们假设要求P[5]字符的next[5]的值,我们就要判断P[4]与P[next[4]]是否相等,即P[4]与P[2]是否相等,发现不等。那我们发现1-4共四个字符组成的字符串的前后缀的相似度一定不超过next[4],接下来我们退而求其次,从矮的里面找高个子,判断P[4]与P[next[2]],判断P[4]与P[1]是否相等,终于相等了。那么1-4组成的字符串前后最长相似度是1,next[5]=2。
总结:求next[j],先判断P[j-1]与P[next[j-1]]是否相等,若相等,next[j]=next[j-1];不相等,再判断P[j-1]与P[next[next[j-1]]]是否相等,若相等,next[j]=next[next[j-1]]+1;以此类推…之所以能无限套娃,是因为你在解递归。
上述就是在告诉你这个用到递归思想。
如果上面你都看懂了,知道了使用到递归了,那么自己都可以把代码写出来了,更不用说理解了。
代码如下(示例):
void GetNext(char T[],int *next)
{
int j=1,k=0;
next[1]=0;
while(j<T[0])
{
if(j==0||T[j]==T[k])
{
++j;
++k;
next[j]=k;
}
else
k=next[k];
}
}
还有一点是if()语句里的j==0:这里有两种情况会用到:
1、next[1]=0,next[2]=1,这是一定的。
2、求next[j]值时,我们要反复判断P[j-1],最坏的就是j-1位置的字符和j-1之前的字符都不相等,在判断的时候一定会经历一步是P[j-1]与P[1]比较(一时想不到,找个例子算一算)。当P[j-1]与P[1]不相等,k就等于0了,这是if()语句中的j与0判断的作用了。此时说明P[j-1]和前面谁都不相同,那么next[j]=1。在公式上,这种情况属于其他情况。
三、计算出与主串匹配的位置
int Index(String S, String T, int pos){
int i = pos;
int j = 1;
int next[255];
Get_NextVal(T, next);
while (i<=S[0] && j<=T[0]){
if (j=0 || S[i]==T[j]){
i++;
j++;
}
else{
j = next[j];
}
}
if (j > T[0])
return i-T[0];
else
return 0;
}
引用:《大话数据结构》
如果next[]数组中递归思想搞清楚了,会发现子串T[]与主串S[]的比较还是递归。
总结
个人感觉说的罗里吧嗦的,但是这是个人学习时反复琢磨的思路,全写在上面了。想到KMP算法用到了递归,其实就成功了!