初探KMP算法

算法简述

KMP是单模匹配算法,在一个长度为n的文本串中查找一个长度为m的模式串。它的时间复杂度是O(m+n),是这类算法的理论最优复杂度。
模式匹配:在长为n的字符串S中,找到某个长为m的模式串P,复杂度至少是O(m+n)。

朴素的模式匹配算法

普通的暴力方法是从S中逐个字符开始匹配P中每个字符。最好复杂度是O(m+n),例如S = "abcdefg123", P = "123",只需处理7+3=10次。最差复杂度是O(mn),例如S = "aaaaaaab" ,P = "aab",要匹配5*3+3=18次。

KMP算法

KMP是在任何时候都能达到O(m+n)复杂度的算法。它通过预处理P,使得在与S匹配时能够跳过一些字符,达到快速匹配的目的。

算法原理

KMP中,在比较S[i],P[j]时,i指针会走到S串尾不回溯,而只回溯j指针达到快速匹配目的,其是通过KMP的核心next[]数组做到,回溯时next[]数组会给出j回溯的位置。

next数组

next[i+1] = P[i]之前(包括i)相同前后缀的最大长度,从而做到在S[i+1]!=P[j]后回溯时,通过j = next[j]就可以回到P[j-1]之前(包括j-1)相同前后缀的前缀末端+1的位置开始匹配。
我们可以通过如下代码得到next[]数组,复杂度为O(m):

void getNext(char *p,int plen){
    Next[0] = -1;
    for(int i = 0;i<plen-1;i++){
        int j = Next[i];
        while(~j && p[i] != p[j]) j = Next[j];
        Next[i+1] = ~j?j+1:0;
    }
}
//next[i+1] = p[i]之前(包括i)的公共前后缀最大长度,j代表前缀末端
//每次在p[i]匹配前后缀时,若匹配则在next[i+1]存放前缀长度j+1(即从这个位置可回溯并匹配的最近下标),不匹配则回溯到next[j](最近的公共前后缀串下标+1)

用next[]数组进行匹配

得到next数组后,我们只需要在S[i],P[j]匹配失败时将指针j回溯到next[j]的位置,继续匹配即可。
实现如下,复杂度为O(n):

int kmp(char *s,char *p){
    int cnt = 0;
    int slen = strlen(s);
    int plen = strlen(p);
    getNext(p,plen);
    int j = 0;
    for(int i = 0;i<slen;i++){
        while(~j && s[i] != p[j]) j = Next[j];
        j++;
        if(j == plen){
            ans[cnt++] = i - plen + 1;
            i--;
            j = Next[j-1];
        }
    }
    return cnt;
}
//求next数组
//匹配字符串s,p.在s[i]处不匹配,则j=next[j]回溯到最近的公共前后缀串下标+1处(即用s已匹配部分的后缀去找p的已匹配部分的前缀相同的最大长度并从后继续匹配)
//在s[i]匹配,则i+1,j+1继续匹配
//匹配完一个,j=next[j]回溯到最近的公共前后缀串下标+1处,继续匹配

完整代码

#include <bits/stdc++.h>
#define TLE std::ios::sync_with_stdio(0),cout.tie(0),cin.tie(0)

using namespace std;

char s[1010],p[1010];
int Next[1010];
int ans[1010];


void getNext(char *p,int plen){
    Next[0] = -1;
    for(int i = 0;i<plen-1;i++){
        int j = Next[i];
        while(~j && p[i] != p[j]) j = Next[j];
        Next[i+1] = ~j?j+1:0;
    }
    cout<<"Next:";
    for(int i = 0;i<plen;i++){
        cout<<Next[i]<<' ';
    }
}
//next[i+1] = p[i]之前(包括i)的公共前后缀最大长度,j代表前缀末端
//每次在p[i]匹配前后缀时,若匹配则在next[i+1]存放前缀长度j+1(即从这个位置可回溯并匹配的最近下标),不匹配则回溯到next[j](最近的公共前后缀串下标+1)


int kmp(char *s,char *p){
    int cnt = 0;
    int slen = strlen(s);
    int plen = strlen(p);
    getNext(p,plen);
    int j = 0;
    for(int i = 0;i<slen;i++){
        while(~j && s[i] != p[j]) j = Next[j];
        j++;
        if(j == plen){
            ans[cnt++] = i - plen + 1;
            i--;
            j = Next[j-1];

        }
    }
    return cnt;
}
//求next数组
//匹配字符串s,p.在s[i]处不匹配,则j=next[j]回溯到最近的公共前后缀串下标+1处(即用s已匹配部分的后缀去找p的已匹配部分的前缀相同的最大长度并从后继续匹配)
//在s[i]匹配,则i+1,j+1继续匹配
//匹配完一个,j=next[j]回溯到最近的公共前后缀串下标+1处,继续匹配


int main(){
    TLE;
    cin>>s>>p;
    int cnt = kmp(s,p);
    cout<<endl<<"Match:"<<cnt<<endl;
    cout<<"Location:";
    for(int i = 0;i<cnt;i++){
        cout<<ans[i]<<' ';
    }
    return 0;
}
posted @ 2022-01-21 23:32  空白菌  阅读(93)  评论(0编辑  收藏  举报