kmp算法小结

首先来说说kmp算法用在什么问题中:

给你两个字符串a和b,问b在a中是否出现过

在这个问题中,我们称a为文本串,b为模式串(只是个名字,并没有什么特殊意义。。),这种问题第一眼看过去就能想到打暴力嘛,但是,时间复杂度为n*m,未免高了点,那怎么办呢?先看看暴力匹配时的弱点在哪里:

假如给你模式串s=abac,文本串ss=abad……(后面的不重要了),那么暴力打法就会将其一位一位的匹配,前三位还是匹配成功了的,但是,却发现在第四位匹配失败了,那么只好从头开始,从模式串的第一位,文本串的第二位再次尝试,但是,我们之前已经知道了,s[1]=ss[1],s[2]=ss[2],s[3]=ss[3],并且s[1]!=s[2],那么从模式串的第一位,文本串的第二位再次尝试,我们早就知道是不会成功的,但是s[1]=s[3],也就是说可以将模式串的指针跳到s[2]处,文本串的指针依然指向ss[4],这样我们就相当于省掉了匹配s[1]和ss[3]以及匹配s[1]和ss[2]的时间,那么这种算法,就叫做“KMP算法”!从它的匹配过程就可以看出它的匹配时间只有n+m,也就是线性时间,那么kmp算法怎么实现呢,让我来细细讲解一下:

首先,这里给出KMP算法的核心:next数组,这个东西就是告诉模式串的指针,当你在这一位匹配失败时,应该跳到哪里继续匹配,那么next数组怎么算呢,其实,next[i]就等于模式串1~i这一段字符串的长度最大的相同的前缀和后缀的长度,就比如下面这个例子:


这样总能理解next数组的含义了吧,但是仔细想想,在匹配时,假如s[j]这一位匹配失败了,那么其实它应该跳到的地方应该是next[j-1]+1,因为只有前j-1位匹配成功了,所以我们只知道1~j-1的一段后缀与它的一段前缀相同长度,所以只能将模式串右移直到文本串中与它的后缀相同的部分与它的那段前缀重合,然后继续匹配,那么我们不妨将next数组右移一位,然后从0开始编号,设next[0]=-1,用来判断当next[j]=-1时就不往回跳了,因为这时候j=0,没地方跳了,只能将文本串的指针向前一位,改完之后就是这个样子的:

可以发现,改完之后是没有多大改变的,那么next数组怎么求呢?总不能枚举前缀和后缀来求吧,这样的话时间复杂度会很高,求next其实只需要用到递归即可,next[0]初始化为-1,next[1]初始化为0(思考一下为什么),然后对于next[j],其实可以由next[j-1]求得,令k=next[j-1],每一次判断s[j]和s[k+1]是否相等,如果相等,next[j]=next[j-1]+1,否则k=next[k],因为s[j]就是1~s[j-1]的最长相同前缀和后缀中的后缀上再加一个字符,那么我们只需要判断在它的前缀后面也加一个字符(也就是加上s[next[j-1]+1])后他们是不是依然相等,如果相等就让next[j]=next[j-1]+1,否则令k=next[j-1],让k=next[k],继续尝试匹配,直到成功为止,可以知道,这样匹配的话第一次匹配成功时的长度一定是最长的,因为next数组越往前越小,也就是k是会越来越小的,如果不懂为什么的话再仔细回忆一下next数组的定义,然后思考或模拟一下就好了。

那么,上代码吧!(这份代码回答的问题是模式串在文本串中出现了多少次,如果想要上面那个问题的代码只需要在此基础上稍作更改即可)

#include <cstdio>
#include <cstring>

char s[1000010],ss[1000010];
int next[1000010];

int main()
{
    scanf("%s",s);//文本串 
    scanf("%s",ss);//模式串 
    int n=strlen(s);
    int m=strlen(ss);
    next[0]=-1;
    next[1]=0;//初始化 
    for(int i=1;i<m;i++)
    {
        int j=next[i];//由next[j]推出next[j+1],和从next[j-1]推出next[j]是一个道理 
        while(j!=-1&&ss[i]!=ss[j])j=next[j];//如上所述,直到匹配成功为止 
        next[i+1]=j+1;
    }
    int i=0,j=0;//i为文本串的指针,j为模式串的指针 
    int ans=0;//记录出现次数 
    while(i<n)
    {
        if(j==-1||s[i]==ss[j])//假如匹配完全失败(完全失败指一位都没有匹配成功,也就是当j=0时都没有匹配成功,这时候j=next[j]就会使得j=-1)或者是匹配成功 
        {
            i++;//这里可能有人会问,匹配完全失败时为什么跟匹配成功时的操作一样呢?匹配成功时为什么i++j++就不用讲了吧 
            j++;//那么当完全失败时,j是不是要变成0,然后i往后跳一位,从头开始重新匹配,那么完全失败时,j=-1,j++后不就是0了吗,那i++对应的就是i往后跳一位 
            if(j==m)//假如匹配成功 
            {
                ans++;
                j=next[j];//依然跳回去重新匹配 
            }
        }
        else j=next[j];//匹配失败,跳回去 
    }
    printf("%d",ans);
}


阅读更多

没有更多推荐了,返回首页