关于《数据结构》课本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[]数组是有一定缺陷的,下面进行举例:

 

  KMP_e

 

  如上图,如果按照之前的方法所获取的next[]数组的话,当两个字符串匹配到上图的情况是,将会出现如下图的情况:

KMP_e2

  我们发现,从step1到step3所走的路都是浪费的,因为都是用同一个字母(a)和b去比,而这个计算机也是很容易识别的,所以对于

next[]的改进是行的通的。

  究其原因,为什么我会说上面的3个步骤是白走的呢,以为这是三个连续的相等的a,因此我们可以从第一步直接跳到第四步,即:得到的数组next[j] = k,而模式串p[j] = p[k],当主串中的s[i] 和 p[j] 匹配失败时,不需要再和p[k]比较,而直接和p[next[k]]进行比较,当然可以一直迭代往前。

即:

  KMP_e3

代码如下:

 

 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数组。

posted @ 2018-10-15 21:46  王陸  阅读(571)  评论(0编辑  收藏  举报