从需求出发,理解 KMP 算法

参考资料:《数据结构-C语言版 第二版》 严蔚敏、李冬梅、吴伟民 编著

                        「天勤公开课」KMP算法易懂版 【up主:天勤率辉

                        最浅显易懂的 KMP 算法讲解 【up主:奇乐编程学院

                        帮你把KMP算法学个通透! 【up主:代码随想录

        先以文本串“aabaabaaf”,模式串“aabaaf”为例。介绍比较基础的“笨办法”(熟悉BF算法的可以立马跳过)

  • 第一次尝试匹配

下标123456789
主串aabaabaaf
模式串aabaaf
此处不匹配
  • 第二次尝试匹配(不匹配时模式串无脑右移一位)

下标123456789
主串aabaabaaf
模式串aabaaf
此处不匹配
  • 第三次尝试匹配

下标123456789
主串aabaabaaf
模式串aabaaf
此处不匹配
  • 第四次尝试匹配

下标123456789
主串aabaabaaf
模式串aabaaf
匹配成功

总结:

        在用“笨办法”匹配的过程中,我们发现,第一次不匹配时,我们就可以获得信息:主串前5位一定为“aabaa”,且利用这一信息就可以得到第二、第三次匹配就是“多余的”。

        由此我们提出需求:如何能跳过不必要的匹配呢?

        在第一次匹配不成功后,我们可以将文本串抽象地看成

下标123456789
文本串aabaa

        利用这一信息就可以判断第二、第三次匹配一定失败,过程如下:
                

        原第二次匹配(抽象后)        

下标123456789
主串aabaa
模式串aabaaf
不匹配
        原第三次匹配(抽象后)
下标123456789
文本串aabaa
匹配串aabaaf
不匹配

        不难发现,这里产生的信息,是可以由模式串本身得出的。也就是说,事先得出模式串的所有可能的不匹配点并找到对应的优化策略,即可在匹配时跳过不必要的匹配。

        因此,我们接下来尝试做一次匹配前优化,也就是生成next数组。

        如果模式串第一个就不匹配,当然不用优化,模式串必定得右移一位,但此时主串下标也应右移一位,因为如果主串与模式串如果第一个就不匹配,那么他们的匹配起始点一定不是当前下标 j 。这里要弄清楚,KMP算法主要通过移动模式串的指针来实现回溯,一开始的“笨办法”是主串指针和模式串指针一起移动的,在KMP算法中,除非主串的匹配子串不是从当前指针位置开始(如模式串第一个就不匹配),否则主串的指针只需不动,模式串的指针移动就相当于模式串向右移动。

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。

模式串a

        如果模式串第二个不匹配,由于我们不知道不匹配的字符具体是哪个,因此用“?”代替(反正它一定不是“a”,这里的一定不是“a”的信息用于后面优化next数组)

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。a

模式串aa

        显然这里也得右移一位,这里的右移与第一次不匹配的右移有些许不同,这里右移只需移动模式串即可,第一个不匹配时主串和模式串都需要右移。

        如果模式串第三个不匹配,同样地

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。aa
模式串aab

        此时模式串仍该右移一位,但含义同样发生了变化,因为我们不知道匹配的主串[j+2]是不是"a",为了防止漏解,因此得右移一位检查主串[j+2]是不是“a”。

         如果模式串第四个不匹配时       

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。aab
模式串aaba

                将模式串尝试右移一位,显然不行(如下表)

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。aab
模式串aa       b
不匹配

                再右移一位,还是不行(如下表)

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。aab
模式串a       a
不匹配

后面分析过程略
         综上:如果当模式串第四个不匹配时,向右移动一位与两位还是不匹配,因此可以优化掉这两步过程。
         由此得出,当模式串第四个不匹配时,我们就可以直接将模式串右移三位,也就将我们提出的问题解决了。一开始的模式串匹配问题就可以直接化简为两步。       

        同理,匹配串第五个匹配时,可以直接移动3位。匹配串第六个不匹配时,可以移动3位。这里不做分析,为后面做铺垫

        现在我们就可以用“笨办法”优化“笨办法”了。上代码!!

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>

#define MAX (int)(1e1+5)
#define _for(i,n) for(int i=1;i<=n;i++)

int next[MAX];
int IndexKMP(char* str ,char* t_str,int p);//KMP算法查找是否匹配子串
void PrintNext(int n);//输出next数组
void GetNextFoolVersion(char* t_str);/“笨办法”求next数组
void GetChar(char* m_str,char* t_str,int i,int j);//复制字符串m_str的第i到j字符至t_str

int main(int argc, const char * argv[]) {
    // insert code here...
    char t_str[]=" aabaaf";
    char m_str[]=" aabaabaaf";
    GetNext(t_str);
    PrintNext((int)strlen(t_str)-1);
    printf("匹配起始点:%d\n",IndexKMP(m_str, t_str, 1));
    return 0;
}

int IndexKMP(char* str ,char* t_str,int p){
    //利用模式串t_str的next函数求t_str在主串str中第p个字符之后的位置
    //其中,t_str非空,1<=p<=str.length
    int i=p,j=1;
    while(i<strlen(str) && j<strlen(t_str)){
        //两个字符串皆未匹配到串尾,则继续比较后续的字符串
        if(j==0 || str[i]==t_str[j]){
            printf("%c=%c\n",str[i],t_str[j]);
            ++i;++j;
        }else{
            j=next[j];
        }
    }
    if(j>strlen(t_str)-1){
        return i-(int)strlen(t_str)+1;
    }else{
        return 0;
    }
}

void GetNextFoolVersion(char* t_str){
    //"笨办法"生成next数组
    //第一个与第二个不匹配不用算,必定右移一位,因此从第三个不匹配开始算起
    next[1]=0;//这里的右移要包括文本串的右移,因此为0。详情见IndexKMP函数if条件判断部分
    next[2]=1;
    for(int i=3;i<strlen(t_str);i++){
        //当第i位不匹配时,匹配串最多可以优化i-2步,因为直接跳过i-1步,就跳出了已匹配的所有字符
        int j=1;
        for(j=1;j<=i-2;j++){
            char *str1,*str2;
            int n=i-1-j;//需要比较的字符串的长度
            str1=(char* )malloc(sizeof(char)*(n+2));str1[0]=' ';str1[n+1]='\0';//如果不这样初始化,默认会在0位上初始化结束符
            str2=(char* )malloc(sizeof(char)*(n+2));str2[0]=' ';str2[n+1]='\0';
            GetChar(t_str, str1, 1+j, 1+j+n-1);
            GetChar(t_str, str2, 1, n);
            if(memcmp(str1, str2, strlen(str1))==0){
                /*  int memcmp(const void *str1, const void *str2, size_t n)
                    把 str1 和 str2 的前 n 个字节进行比较。
                    如果返回值 < 0,则表示 str1 小于 str2。
                    如果返回值 > 0,则表示 str1 大于 str2。
                    如果返回值 = 0,则表示 str1 等于 str2。*/
                next[i]=i-j;
                break;
            }
        }
        if(j==i-1){
            next[i]=1;
        }
    }
}

void GetChar(char* m_str,char* t_str,int i,int j){
    int n=1;
    for(;i<=j;i++){
        t_str[n++]=m_str[i];
    }
}



void PrintNext(int n){
    _for(i,n){
        printf("next[%d]=%d\n",i,next[i]);
    }
}

        可能一开始看不懂这个next数组怎么求出来的,next数组的值具体是什么意思,变量i,j什么意思,可以在单步调试稍微理解代码的基础上继续往后看。

        从代码里可以看出“笨办法”求next数组代码量很大(或许有更简洁的方法),当匹配串也十分长时,“笨办法”时间复杂度很高的问题会在长匹配串的情况下暴露出来,因此我们得完全放弃“笨办法”。

        我们可以发现上述的“笨办法”生成next数组过程其实在寻找最长公共前后缀。

        如字符串“aabaaf”的前缀有:

        “a” ,“aa”,“aab”,“aaba”,“aabaa”

        后缀有:

        “f”,“af”,“aaf”,“baaf”,“abaaf”

        前后缀不会是整个字符串

       

        补完前后缀的基础,现在以上面留的铺垫为例(当匹配串“aabaaf”的第五个不匹配时,它的next数组怎么找)

如果模式串第四个不匹配时       

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。aaba
模式串aabaa

尝试右移一位

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。aaba
模式串aaba

它就是在比较,同为长度三的前缀“aab”与后缀“aba”是否相同,显然不相同,因此不匹配

所以继续右移一位

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。aaba
模式串aab

这次就是在比较,同为长度为二的前缀“aa”与后缀“ba”,显然也不同

因此继续右移

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。aaba
模式串aa

这次就是在比较,同为长度为1的前缀“a”与后缀“a”,这回相同了。

因此当匹配串第五位不匹配时,匹配串可以直接向右移动三位。也就是将文本串指针固定在j+4,匹配串的指针回溯到2。(这里理解了,也就能看懂代码next数组值的含义。)

那么,我们大概知道next数组中的数值其实就是已匹配成功子串的最长公共前后缀的长度+1

因此我们将上面用“笨办法”求next数组的方法转换为,找最长公共前后缀问题。如下:

当第二个未匹配时,就要找“a”的最长公共前后缀,很显然没有,       next[2]为1

当第三个未匹配时,就要找“aa”的最长公共前后缀,是“a”                  next[3]为2              

因为最长公共前后缀长度为1,当匹配串第三位不匹配时,需要检查匹配串第二位是否匹配,因此next为2(最长公共前后缀长度+1)

当第四个未匹配时,就要找“aab”的最长公共前后缀,没有                   next[4]为1

当第五个未匹配时,就找“aaba”的最长公共前后缀,是“a”                  next[5]为2

当第六个未匹配时,就找“aabaa”的最长公共前后缀,是“aa”              next[6]为3

接下来在解释一个next数组求法的联系

当第五个未匹配时,我们已经知道“aaba”的最长公共前后缀长度是1

那么在第六个未匹配时,“aabaa”的最长公共前后缀其实是在前一结论的基础上查找的,也就是后缀“成长”为“aa”对应的前缀“aa”是否相同。因为之前的后缀“aba”与前缀“aab”已经不相同,后缀即使“成长”为“abaa”对应前缀“aaba”也一定不相同。

在后缀成长的时候有两种情况

1. 后缀的新增的字符与前缀对应的字符相同:如第五个next值,最长公共前后缀为“a”,到第六个next值在第五次上查找,后缀新增“a”与前缀对应的“a”刚好一样,那么next[6]=next[5]+1

2. 后缀的新增的字符与前缀对应的字符不同:如第三个next值,最长公共前后缀为“a”,但在求第四个next值时,后缀新增“b”与对应的前缀“a”不一致,那么这里就可以看成模式串第二个不匹配问题:

        求完next[3]后的状态

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。aa
模式串aa       

        求next[4]时的初始状态

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。aab
模式串aa       

        不匹配,就可以抽象看成“模式串第二个不匹配问题”:

下标。。。jj+1j+2j+3j+4j+5j+6j+7
主串。。。a
模式串aa       

        模式串接下来下标得移动到next[2]的位置上检查前后缀是否相同​​​​​​​

因此我们由此可写出简洁的next数组求法,上代码!!

void GetNext(char* t_str){
    //生成模式串的next数组
    int i=1,j=0;
    next[1]=0;
    while(i<strlen(t_str)){
        if(j==0 || t_str[i]==t_str[j]){
            ++i;++j;
            next[i]=j;
        }else{
            j=next[j];
        }
    }
}

这个代码可直接粘在之前的程序中,加上对应的声明即可。

——————————————分割线————————————(后面内容待完善)

下面以模式串“abaabcac”为例

j

12345678
模式abaabcac
next[j]0112232

next[1]=0这是默认的

因为“模式串”在匹配“主串”的时候,当“模式串”的第一个字符串就不匹配时,“主串”如果包含该“模式串”,就一定不是以第该位置为起点。因此“主串”的指针得移动到下一位。具体为什么是0,就得结合后面的代码一起理解。

“模式串”应回溯到起点,从头开始匹配(即主串与模式串的当前匹配成功字符个数为0),想想如果这里回溯到1位置的话,会造成主串的指针还没后移,模式串的指针已经移动到第一个位置了,因此为了配合主串指针移动到下一个的同时,模式串也刚好移动到第一位,因此0是比较合适的。

例如:

主串.....b
模式串a
第一个就没匹配

如果第二个没有匹配会怎么办呢?

主串.....ac
模式串ab
第二个没有匹配

第一种算法会将这个情况,抽象地看成这样

主串.....a
模式串ab
第二个没有匹配,将这个情况抽象化

这时我们知道主串第i个“?”不和模式串第j个“b”匹配,因此我们接下来应该看看它是否能和第1个“a”匹配,我们不知道这个“?”是不是“a”,因此得抱着试一试的心态去看看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值