KMP算法

目录

BF算法

KMP算法

首先举例,为什么主串不回退?

j回退的位置

next数组

next数组的优化


在讲解KMP算法之前,先说一说BF算法。

BF算法

BF算法是一种暴力的字符串匹配算法,假设主串是s,用i去维护匹配的位置,模式串是sub,用j去维护匹配的位置。

i、j均从字符串开头开始匹配,每次匹配失败后,i回退到s开始匹配的位置的后一个位置,j又从sub开头开始匹配。

M*(N-M+1),O(M*N)

KMP算法

KMP算法是一种改进的字符串匹配算法。它的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。

KMP和BF不一样的地方在:主串的i并不会回退,j也不会每次直接移动到0号位置。

首先举例,为什么主串不回退?

 假设目前在2号位置匹配失败,就算让i按BF算法的规则回退到1位置,也是不必要的,因为b与子串0位置的a也不一样。

j回退的位置

此时匹配失败,我们不进行回退i,因为在这个地方匹配失败,说明i的前面和j的前面是有一部分相同的,不然两个下标不可能走到这里。

我们发现如果j回退到2下标而i不回退,这就是最好的情况了,问题就是怎么知道j回退到2下标?

next数组

KMP的精髓就是next数组,也就是用next[j]=k来表示不同的j对应的一个k值,这个k就是将来j位置不匹配时j回退到的位置。

求k的规则:

1、找到两个相等的真子串(不能是同一个,但可以部分重合),一个以下标0开始,一个以j-1下标结束,k值就是真子串的长度。

2、找不到,next[j]=0。

3、不管什么数据,next[0]=-1,next[1]=0

(如果有next[0]=0,next[1]=1,那么把我们按这个规则求得的next数组统一+1即可)

首先两个串能匹配到黑线位置处,说明模式串前面都与主串匹配上了,所以绿线部分一定是相同的,然后我们再按求k的规则,假设存在,那么我们就可以找到蓝线部分的字符串与绿线相同,所以最好的情况就是i不回退,将j回退到红线位置,继续去比较红线处的字符与主串黑线处的字符。k值是真子串的长度的原因是蓝串从下标0开始,那么红线字符的下标就是真子串的长度。

 求next数组的练习

 

我们可以发现,next数组增大时,只能+1;不存在两个相同的子串时,不一定回退到0,下面证明: 

已知next[i]=k,怎么求next[i+1]?

因为next[i]=k,那么就有s0...sk-1==sx...si-1;由于k-1-0==i-1-x,所以x==i-k,

所以s0...sk-1==si-k...si-1,

如果sk==si,那么s0...sk==si-k...si,next[i+1]=k+1。

如果sk!=si,

 那么k要继续回退,直至sk==si,或者k越界,回退到0位置,此时k=0,next[i+1]=k+1=1。

void Getnext(vector<int>& next,string& sub){
    next[0]=-1;
    if(sub.size()==1) return;
    next[1]=0;
    int i=1,k=next[1];
    //不能用for循环i++,因为在k往前找时,i不应该动
    while(i<sub.size()){
        //k有可能等于-1,代表没有两个完全相同的子串,此时next[i]=0
        if(k==-1||sub[i]==sub[k]){
            i++;
            k++;
            next[i]=k;
        }
        else{
            k=next[k];
        }
    }
}
//从主串s的pos位置开始查找sub
int KMP(string& s,string& sub,int pos){
    if(s.empty()||sub.empty()||s.size()<sub.size()) return -1;
    //i控制主串的匹配的位置,不能用for循环i++,因为在匹配不上的时候i是不动的
    int i=pos,j=0;
    vector<int>next(sub.size()+1);
    Getnext(next,sub);
    while(i<s.size()&&(j==-1||j<sub.size())){
        //next[0]=-1,j有可能等于-1,表示没有能和s[i]匹配上的,也需要进入
        //不能只写j<sub.size()是因为sub.size()返回的是一个无符号数
        if(j==-1||s[i]==sub[j]){
            i++;
            j++;
        }
        else{
            j=next[j];
        }
    }
    if(j>=sub.size()) return i-j;
    return -1;
}

next数组的优化

如这个例子, 如果主串是aaaaaaaac,当b与c不匹配的时候,模式串的j按照next数组会从b回退到7号位a,c与a仍然不匹配,j继续回退到6号位a,以此类推最终到-1。因为回溯后的字符与原字符相同,原字符不匹配,回溯后的字符自然也不匹配,所以我们可以对上述情况进行优化。

规则:

回退到的字符与当前位置的字符相同,当前位置的nextval就写回退到的那个字符的nextval。

s[i]==s[next[i]],nextval[i]=nextval[next[i]]

回退到的字符与当前位置的字符不相同,当前位置的nextval就写当前位置的字符的next。

s[i]!=s[next[i]],nextval[i]=next[i]

void Getnextval(string& sub,vector<int>& nextval){
    nextval[0]=-1;
    if(sub.size()==1) return;
    int i=0,k=nextval[0];
    while(i<sub.size()){
        if(k==-1||sub[i]==sub[k]){
            //本来是next[i]=k+1
            i++;
            k++;
            if(sub[k]==sub[i]){
                nextval[i]=nextval[k];
            }
            else{
                nextval[i]=k;
            }
        }
        else{
            k=nextval[k];
        }
    }
}
int KMP(string& s,string& sub,int pos){
    if(s.empty()||sub.empty()||s.size()<sub.size()) return -1;
    int i=pos,j=0;
    vector<int>nextval(sub.size()+1);
    Getnextval(sub,nextval);
    while(i<s.size()&&(j==-1||j<sub.size())){
        if(j==-1||s[i]==sub[j]){
            i++;
            j++;
        }
        else{
            j=nextval[j];
        }
    }
    if(j>=sub.size()) return i-j;
    return -1;
}

例题:

训练赛20190304 - Virtual Judge (vjudge.net)

一行多次匹配(可重叠)

#include<iostream>
#include<vector>
#include<string>
using namespace std;
string sub,s;
void Getnextval(vector<int>& nextval){
    nextval[0]=-1;
    if(sub.size()==1) return;
    int i=0,k=nextval[0];
    while(i<sub.size()){
        if(k==-1||sub[i]==sub[k]){
            i++;
            k++;
            if(sub[i]==sub[k]){
                nextval[i]=nextval[k];
            }
            else{
                nextval[i]=k;
            }
        }
        else{
            k=nextval[k];
        }
    }
}
int KMP(){
    int num=0;
    if(sub.empty()||s.empty()||s.size()<sub.size()) return num;
    vector<int>nextval(sub.size()+1);
    Getnextval(nextval);
    int i=0,j=0;
    while(i<s.size()){
        if(j==-1||s[i]==sub[j]){
            i++;
            j++;
        }
        else{
            j=nextval[j];
        }
        if(j==sub.size()){
            num++;
            j=nextval[j];//可重叠
            //j=0;//不可重叠
        }
    }
    return num;
}
void solve(){
    cin>>sub>>s;
    cout<<KMP()<<endl;
}
int main(){
    int t;
    cin>>t;
    while(t--){
        solve();
    }
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值