• 关于《数据结构》课本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
    ",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数组。

  • 相关阅读:
    django学习笔记1
    排序多重排序
    06计算列
    填充日期序列
    行,列单元格
    读取excel文件
    监控文本
    天干地支纪年法
    Mysql基础
    JDBC基础
  • 原文地址:https://www.cnblogs.com/wkfvawl/p/9794954.html
Copyright © 2020-2023  润新知