KMP算法图文详解

KMP算法详解

首先解释一下什么KMP算法:

KMP算法要解决的问题就是在字符串(也叫主串)中的模式(pattern)定位问题。说简单点就是我们平时常说的关键字搜索。模式串就是关键字(接下来称它为P),如果它在一个主串(接下来称为T)中出现,就返回它的具体位置,否则返回-1(常用手段)

关于这类问题的解法

  • 暴力法,时间复杂度O( N ∗ M N*M NM)(N为主串长度,M为模式串长度),模式串尝试去匹配主串的每一个位置,直到完匹配成功
    例如:
    当两个字符串进行匹配时,P串从主串T的第一个字符开始匹配,红色位置为第一次出现字符不匹配的位置。此时不匹配字符下标为i=j=3
    在这里插入图片描述
    下一次匹配是P从下标为0开始和T的从下标为1进行匹配,之后一直重复这个过程
    在这里插入图片描述
    这种方法过于暴力,所以咱们要引入一个更快的方法,KMP算法
  • KMP算法 时间复杂度O( N N N) N为主串的长度
    按照正常思维,对于上图中的匹配,当第一次发生不匹配时,自然会想到把P串中的a和T串中的下标为3的字符即a字符进行匹配,看下图
    在这里插入图片描述
    即,T串中i下标不变,P串中j变为0,即利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置
    让我们再来再来看一组数据
    在这里插入图片描述
    当T和P在下标为3处发生不匹配,按照上边所说,i不动,P串移动到有效位置,下一次开始比的位置是j=2的位置。
    在这里插入图片描述
    其实这就像是一个推动的过程,j的位置由P中与T中发生不匹配位置之前的字符串中最长公共前缀后缀的值来决定

首先解释一下前缀和后缀,以此字符串为例
在这里插入图片描述
前缀为含有第一个字符,且不包含最后一个字符的所有连续子串,如下图所示
在这里插入图片描述
后缀为含有最后一个字符,且不包含第一个字符的所有连续子串
在这里插入图片描述
在这里插入图片描述
这里我要对模式串求一个next数组,next[i]代表,前i-1个字符的最长公共前缀后缀的值。人为规定next[0]=-1,next[1]=0;

  • 因为当只有一个字符的时候,其之前没有字符,所以说无意义。
  • 当有两个字符的时候,以上方P串为例子,next[1]就是求b之前a的最长公共前缀后缀的值,但由于只有一个字符,按照定义来说前缀不包含最后一个字符和后缀不包含第一个字符,矛盾,所以值为0。

对于next[6]来说,前边abcabc的最长公共前缀和后缀为abc所以next[6]=3

如何求解next数组
从next[2]开始求解,前边已经说过,0和1位置是人为规定的值。下边是next[i]( i>=2 )的求解方式。

  • 首先这里需要规定一个k值,k值代表当前前next[i-1]的值,即前i-2个字符中最长公共前缀和后缀的值
  • 求解next[i]时,需要比较第i-1个字符和第k个字符是否相同
    • 当相同时,next[i]=next[i-1]+1;
    • 否则k=next[k],直到k=0;

举个例子:
在这里插入图片描述
初始值k=0;nex[0]=-1,next[1]=0;
在这里插入图片描述
i=2时,p[1]!=p[k](k=0)即b!=a
k=0//即已经到头了,还没能匹配上,由于k==0,令next[2]=0;结束
在这里插入图片描述
i=3时,p[2]!=p[k](k=0),有因为k= =0,所以令next[3]=0,结束
在这里插入图片描述
i=4时,p[3]==p[k](k=0)所以,令next[4]=++k;即next[4]=1;结束
在这里插入图片描述
i=5时,p[4]=p[k](k=1 ),所以next[5]=++k;即next[5]=2;
在这里插入图片描述
同理得next[6]=3
在这里插入图片描述

但是我跑完一遍才发现。。。这个例子中并没有用到k=next[k]这个用法。那么我来修改一下P串,令下标为5的c变成a,其他不改变
在这里插入图片描述
i=6时,k=2,p[6]!=p[2],k=next[k],即k=0
p[0]==p[6],所以next[6]=++k=1;

可以把P拆成两部分看待
在这里插入图片描述
next数组就算求完啦。
接下来就是KMP算法部分

  • 输入两个字符串s1,s2(s1是主串,s2是模式串)
  • 获取s2的next数组
  • 定义两个指针i1和i2分别代表指向s1的位置和s2的位置
  • 当s1[i1]==s2[i2]时,i1++,i2++
  • 否则 i2=next[i2]
  • i2移到0即next[i2]==-1时,i1++匹配到模式串的第一个也没匹配上,只能和主串的下一个进行匹配了。
  • 最后判断i2是否等于s2的长度,等于返回i1-i2,否则返回-1

例如
在这里插入图片描述
P串next数组已经求出,i1=i2=6时两个字符串发生不同,此时i1不动,i2=next[6]=3,如下图所示
在这里插入图片描述
此时能正好匹配成功,返回i1-i2=3.

下边是代码()

求next数组

void get_next()
{
    
    
    Next[0]=-1;
    Next[1]=0;

    int i=2,k=0;//i是模式串的起始位置,即从第三个字符开始匹配,k是i-1个字符要匹配的位置
    int len=s2.size();
    while(i<len)
    {
    
    
        if(s2[i-1]==s2[k])//如果i-1和k相等,i后移准备匹配下一个位置,k后移
            Next[i++]=++k;
        else if(k>0)//没有匹配成功,k移动到next[k]的位置
            k=Next[k];
        else
            Next[i++]=0;//移动到头了,next[i]只能为0了
    }
}

KMP

int kmp()
{
    
    
    int i1=0,i2=0;
    int len1=s1.size();
    int len2=s2.size();
    get_next();//获得Next数组
    while(i1<len1&&i2<len2)//i1没到头,i2也没到头
    {
    
    
        if(s1[i1]==s2[i2])//相等就齐头并进
        {
    
    
            i1++;
            i2++;
        }
        else if(next[i2]==-1)//模式串到头都没有和主串能匹配的字符,主串往后移
            i1++;
        else
            i2=next[i2];//匹配不成功,i2移动
    }
    return i2==len2?i1-i2:-1;//i2到头证明匹配成功,否则返回-1
}

模板题:HDU-1711
坑点:这个是数字不是字符,用整型数组接收
AC代码

#include<iostream>
#include<cstdio>
#include<string.h>
#include<queue>
#include<cmath>
#include<fstream>
using namespace std;

int Next[200005];
int s1[1000005];
int s2[1000005];
int a,b;
void get_next()
{
    
    
    Next[0]=-1;
    Next[1]=0;

    int i=2,k=0;//i是模式串的起始位置,即从第三个字符开始匹配,k是i-1个字符要匹配的位置
    int len=b;
    while(i<len)
    {
    
    
        if(s2[i-1]==s2[k])//如果i-1和k相等,i后移准备匹配下一个位置,k后移
            Next[i++]=++k;
        else if(k>0)//没有匹配成功,k移动到next[k]的位置
            k=Next[k];
        else
            Next[i++]=0;//移动到头了,next[i]只能为0了
    }
}

int kmp()
{
    
    
    int i1=0,i2=0;
    get_next();
    int len1=a;
    int len2=b;

    while(i2<len2&&i1<len1)//i1没到头,i2也没到头
    {
    
    

        if(s1[i1]==s2[i2])//相等就齐头并进
        {
    
    
            i1++;
            i2++;
        }
        else if(Next[i2]==-1)//模式串到头都没有和主串能匹配的字符,主串往后移
            i1++;
        else
            i2=Next[i2];//匹配不成功,i2移动

    }
    return i2==len2?i1-i2:-1;//i2到头证明匹配成功,否则返回-1
}

int main(void)
{
    
    
    int t;
    cin>>t;

    while(t--)
    {
    
    
        //memset(Next,0,sizeof(Next));

        int ans=kmp();
        if(ans!=-1)
            cout<<ans+1<<endl;
        else
            cout<<ans<<endl;
    }
    return 0;
}

洛谷 P3375
坑点:next数组求是不包含他本身的前n-1个字符的最长前缀后缀值,所以我一开始先加一个没用的字符,之后输出的时候从1开始输出next数组
AC代码

#include<iostream>
#include<cstdio>
#include<string.h>
#include<queue>
#include<cmath>
using namespace std;

int next[2000005];
string s1,s2;
void get_next()
{
    
    
    next[0]=-1;
    next[1]=0;

    int i=2,k=0;//i是模式串的起始位置,即从第三个字符开始匹配,k是i-1个字符要匹配的位置
    int len=s2.size();
    while(i<len)
    {
    
    
        if(s2[i-1]==s2[k])//如果i-1和k相等,i后移准备匹配下一个位置,k后移
            next[i++]=++k;
        else if(k>0)//没有匹配成功,k移动到next[k]的位置
            k=next[k];
        else
            next[i++]=0;//移动到头了,next[i]只能为0了
    }
}

int kmp()
{
    
    
    int i1=0,i2=0;
    get_next();
    s2=s2.substr(0,s2.size()-1);
    int len1=s1.size();
    int len2=s2.size();

    while(i1<len1)//i1没到头,i2也没到头
    {
    
    


        if(s1[i1]==s2[i2])//相等就齐头并进
        {
    
    
            i1++;
            i2++;
        }
        else if(next[i2]==-1)//模式串到头都没有和主串能匹配的字符,主串往后移
            i1++;
        else
            i2=next[i2];//匹配不成功,i2移动
        if(i2==len2)
        {
    
    
            printf("%d\n",i1-i2+1);
            i2=next[i2]; //再次匹配
            i1--;
        }

    }
    //return i2==len2?i1-i2:-1;//i2到头证明匹配成功,否则返回-1
}

int main(void)
{
    
    

    cin>>s1>>s2;
    s2+="$";
    kmp();
    next[0]++;

    int len=s2.size();
    for(int i=1;i<=len;i++)
    {
    
    
        printf("%d ",next[i]);
    }
    return 0;
}

写了一下午,可算是完成了,看过的请给一个小赞吧,谢谢啦

猜你喜欢

转载自blog.csdn.net/Yang_1998/article/details/89764554