关于《数据结构》课本KMP算法的理解
数据结构课上讲的KMP算法和我在ACM中学习的KMP算法是有区别的,这里我对课本上的KMP算法给出我的一些想法。
原理和之前的KMP是一样的https://www.cnblogs.com/wkfvawl/p/9768729.html,但是不同点在于之前的KPM中next数组存放的是到了该位时最大前后缀长度,而这里的KMP中next数组存放的是j下一步需要移动的位置。
个人觉得课本上的KMP算法强调位置,模式串上指针位置j,主串指针位置i,对于位置上的变化,更利于理解代码。
先贴出代码:
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 void getNext(char *p,int *next) 6 { 7 int j,k; 8 next[1]=0; 9 j=1; 10 k=0; 11 while(j<strlen(p)-1) 12 { 13 if(k==0||p[j]==p[k]) //匹配的情况下,p[j]==p[k],next[j+1]=k+1; 14 { 15 j++; 16 k++; 17 next[j]=k; 18 } 19 else //p[j]!=p[k],k=next[k] 20 { 21 k=next[k]; 22 } 23 } 24 } 25 int kmp(char *s,char *p,int *next) 26 { 27 int i=1,j=1; 28 while(i<=strlen(s)&&j<=strlen(p)) 29 { 30 if(j==0||s[i]==p[j]) 31 { 32 i++; 33 j++; 34 } 35 else 36 { 37 j=next[j]; 38 } 39 } 40 if(j>strlen(p)) 41 { 42 return i-strlen(p);///匹配成功,返回存储位置 43 } 44 else 45 { 46 return 0; 47 } 48 } 49 50 int main() 51 { 52 int next[100],ans; 53 char s[20]="ababcabcacbab"; 54 char p[10]="abcac"; 55 getNext(p,next); 56 ans=kmp(s,p,next); 57 printf("%d\n",ans); 58 return 0; 59 }
“利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改 j 指针,让模式串尽量地移动到有效的位置。”
所以,整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道 j 指针要移动到哪?
接下来我们自己来发现j的移动规律:
如图:C和B不匹配了,我们要把 j 移动到哪?显然是第1位。为什么?因为前面有一个A相同啊:
如下图也是一样的情况:
可以把 j 指针移动到第2位,因为前面有两个字母是一样的:
至此我们可以大概看出一点端倪,当匹配失败时,j要移动的下一个位置 k。
存在着这样的性质:
最前面的k个字符和 j 之前的最后k个字符是一样的。
如果用数学公式来表示是这样的
P[0 ~ k-1] == P[j-k ~ j-1]
这个相当重要,如果觉得不好记的话,可以通过下图来理解:
弄明白了这个就应该可能明白为什么可以直接将 j 移动到 k 位置了。
因为:
当T[i] != P[j]时
有T[i-j ~ i-1] == P[0 ~ j-1]
由P[0 ~ k-1] == P[j-k ~ j-1]
必然:T[i-k ~ i-1] == P[0 ~ k-1]
这里我们回忆一下,之前那种KMP算法也是需要移动的, 移动位数 = 已匹配的字符数 - 对应的部分匹配值,已匹配的字符数就是移动到的j位置,而对应的部分匹配值就是前k个字符,一相减得到的不就是k位置吗?
好,接下来就是重点了,怎么求这个(这些)k呢?
因为在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置 j 对应的k,所以用一个数组next来保存。
先看看next数据值的求解方法
位序 1 2 3 4 5 6 7 8 9
模式串 a b a a b c a b c
next值 0 1 1 2 2 3 1 2 3
next数组的求解方法是:
1.第一位的next值为0
2.第二位的next值为1
后面求解每一位的next值时,根据前一位进行比较
3.第三位的next值:第二位的模式串为b ,对应的next值为1;将第二位的模式串b与第一位的模式串a进行比较,不相等;则第三位的next值为1(其他情况均为1)
4.第四位的next值:第三位的模式串为a ,对应的next值为1;将第三位的模式串a与第一位的模式串a进行比较,相同,则第四位的next值得为1+1=2
5.第五位的next值:第四位的模式串为a,对应的next值为2;将第四位的模式串a与第二位的模式串b进行比较,不相等;第二位的b对应的next值为1,则将第四位的模式串a与第一位的模式串a进行比较,相同,则第五位的next的值为1+1=2
6.第六位的next值:第五位的模式串为b,对应的next值为2;将第五位的模式串b与第二位的模式中b进行比较,相同,则第六位的next值为2+1=3
7.第七位的next值:第六位的模式串为c,对应的next值为3;将第六位的模式串c与第三位的模式串a进行比较,不相等;第三位的a对应的next值为1,
则将第六位的模式串c与第一位的模式串a进行比较,不相同,则第七位的next值为1(其他情况)
8.第八位的next值:第七位的模式串为a,对应的next值为1;将第七位的模式串a与第一位的模式串a进行比较,相同,则第八位的next值为1+1=2
9.第八位的next值:第八位的模式串为b,对应的next值为2;将第八位的模式串b与第二位的模式串b进行比较,相同,则第九位的next值为2+1=3
如果位数更多,依次类推
请仔细对比这两个图。
我们发现一个规律:
当P[k] == P[j]时,
有next[j+1] == next[j] + 1
其实这个是可以证明的:
因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)
这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。
即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。
这里的公式不是很好懂,还是看图会容易理解些。
那如果P[k] != P[j]呢?比如下图所示:
像这种情况,如果你从代码上看应该是这一句:k = next[k];为什么是这样子?你看下面应该就明白了。
现在你应该知道为什么要k = next[k]了吧!像上边的例子,我们已经不可能找到[ A,B,A,B ]这个最长的后缀串了,但我们还是可能找到[ A,B ]、[ B ]这样的前缀串的。所以这个过程像不像在定位[ A,B,A,C ]这个串,当C和主串不一样了(也就是k位置不一样了),那当然是把指针移动到next[k]啦。
1 void getNext(char *p,int *next) 2 { 3 int j,k; 4 next[1]=0; 5 j=1; 6 k=0; 7 while(j<strlen(p)-1) 8 { 9 if(k==0||p[j]==p[k]) //匹配的情况下,p[j]==p[k],next[j+1]=k+1; 10 { 11 j++; 12 k++; 13 next[j]=k; 14 } 15 else //p[j]!=p[k],k=next[k] 16 k=next[k]; 17 } 18 }
关于KMP算法的改进:
其实,前面定义的next[]数组是有一定缺陷的,下面进行举例:
如上图,如果按照之前的方法所获取的next[]数组的话,当两个字符串匹配到上图的情况是,将会出现如下图的情况:
我们发现,从step1到step3所走的路都是浪费的,因为都是用同一个字母(a)和b去比,而这个计算机也是很容易识别的,所以对于
next[]的改进是行的通的。
究其原因,为什么我会说上面的3个步骤是白走的呢,以为这是三个连续的相等的a,因此我们可以从第一步直接跳到第四步,即:得到的数组next[j] = k,而模式串p[j] = p[k],当主串中的s[i] 和 p[j] 匹配失败时,不需要再和p[k]比较,而直接和p[next[k]]进行比较,当然可以一直迭代往前。
即:
代码如下:
1 void get_nextval(char *p,int *next) 2 { 3 int j,i; 4 next[1]=0; 5 i=1; 6 j=0; 7 while(i<strlen(p)) 8 { 9 if(k==0||p[i]==p[j]) 10 { 11 i++; 12 j++; 13 if(p[i]!=p[j]) 14 { 15 nextval[i]=j; 16 } 17 else 18 { 19 nextval[i]=nextval[j]; 20 } 21 } 22 else 23 { 24 j=nextval[j]; 25 } 26 } 27 }
关于这里的KMP算法中next数组和之前那种KMP算法中next数组的关系。
既然原理是相同的,这两者必然有一定的联系,我们姑且称最长公共前后缀的那个next为maxl
序号: 1 2 3 4 5 6 7 8
a b a a b c a c
maxl 0 0 1 1 2 0 1 0
next 0 1 1 2 2 3 1 2 ///接下来我们将maxl数组复制一行,去掉最后一个值,在开头加上一个-1,往右平移一位。每个值在+1。得到next数组。
nextval 0 1 0 2 1 3 0 2 ///按序号检查maxl和next的值是否相等,若不相等nextval的值为next的值;若相等,填入对应序号为next值的nextval值。
果然是有着关系的,最长公共前后缀对我来说是比较好理解的,这种方法能够较快的写出next数组。
本文作者:王陸
本文链接:https://www.cnblogs.com/wkfvawl/p/9794954.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步