傻子也能看懂的KMP算法

傻子也能看懂的kmp算法

网上的几个博客,因为太笨,看了之后没有看懂。为了避免我又忘了,在这里写一个博客记录下。这是我能写出来的最通俗易懂的kmp算法了,如果有代码错误的地方请告诉我,不胜感激!

  • 作用:找到主串中有几个串和模式串匹配。

  • 主要思想:按照最糟糕的遍历,每次匹配失败s都回溯到i+1,mt都回溯到0,时间复杂度为O(N*M)(N为主串长度,M为模式串长度),具体的不再细说。

    • 如果想要改进,那么就减少回溯。

    • 怎么减少回溯呢?

    在这里插入图片描述

    当模式串mt匹配到’d’的时候,匹配失败,最坏的方法,是让s的指针重新指到b,mt的指针指到开头的a,再重新作比较。但是从图里我们可以看出,虽然匹配失败了,但是匹配失败的a,前面有a,b,这两个字母和mt开头的a,b是相同的。那么我就可以从匹配失败的a开始,和蓝色指针指向的a作比较,看看它们是否匹配。

    这种方法,使得s不用回溯,mt尽可能小的回溯,大大降低了时间复杂度。

    • 如何让计算机能够读懂这种操作?

    • next数组

      在进行匹配之前,我们要建立一个数组next[M],来记录mt里每位字符的某种“信息”,这种信息能对我们减少回溯进行指导。这个信息是什么呢?就是我们可以根据next数组,得到不匹配的时候,指针应该回溯到的位置。next数组存的实际值是最大的前缀=后缀的长度。

      next数组初始化思路:首先,next数组的长度是mtlen,即模式串的长度,首先将next数组初始化为-1。对于第i 个元素,首先找到nxt[i-1](记为temp)的值,看看nxt[i-1]+1那个位置的字符是不是和第i个元素对应的字符相等,如果相等,那么nxt[i]的值就是temp+1了,即截止到i,前缀=后缀的最大长度是temp+1。

在这里插入图片描述
如果不等呢?那也不用放弃!我们可以继续看看nxt[temp]+1对应的字符能不能和第i个元素匹配上!因为mt[i-1],mt[temp],mt[nxt[temp]]里面存储的字符一定都是相同的,我们看到mt[i] = b, mt[temp+1] = c,不相等。看来cacac≠cacab了。但是呢,万一nxt[temp]+1的元素对应的字符,等于i对应的字符呢?也就是ca? = cab。这样一直向前找,从mt[i]可能的最大值不断寻找……

在这里插入图片描述

那么什么时候停止呢?显然,当temp没法向前找的时候,好像就可以停止了!那么,条件是mt[temp]>-1吗?此时temp就没办法再向前找了。但是!万一人家mt[i]等于mt[0]呢?就像这样:

在这里插入图片描述

所以,将条件改为temp>-1,就相当于又跑去看了看mt[i]和mt[0]是不是相等。

如果这也不等,那就只好让nxt[i] = -1了。

代码如下:

  • int nxt[Max_n];
    void init(){
        nxt[0] = -1;
        int mtlen = strlen(mt);
        for (int i=1;i<mtlen;i++){
            int temp = nxt[i-1];
            while(mt[temp+1] != mt[i] && temp > -1){
                temp = nxt[temp];
            }
            if (mt[temp+1]==mt[i]) nxt[i] = temp + 1;
            else nxt[i] = -1;
        }
    }
    
  • 利用next数组进行回溯

  • 算出next数组后,KMP已经学会一大半啦!现在只要利用它进行操作就可以了。一开始,指针i指向s开头,指针j指向mt开头。如果字符匹配,那么i++,j++,没有啥好讲的。如果匹配的过程中失败了,那么就需要回溯,这个时候就可以用到next数组了。

  • 如果是在j==0的时候匹配失败,i直接++。

在这里插入图片描述

如果j!=0,j = nxt[j-1]+1; 这个语句的意思是,直到j-1的时候,还是匹配的,那就让j回溯,看看next[j-1]后面的那个元素是不是和s[i]匹配。

在这里插入图片描述

  • 如果匹配成功,直到j==mtlen,那就说明我们找到了一个答案,answer可以++了。注意此时的操作:**j进行了和匹配失败时相同的回溯操作。**为什么要这样呢?这个时候,我们是把已得到答案的一部分继续使用了。就是说,两个答案有重叠。这样,它们就共用了一部分。

在这里插入图片描述

此时j进行了部分回溯,从末尾的4到了2位置。相当于两个答案共用了aba。这个可以具体问题具体分析,看题目要不要求进行这样共用一部分的操作。

利用next数组进行回溯操作的代码如下:

  • void solve(){
        int slen = strlen(s), mtlen = strlen(mt), answer = 0;
        for (int i=0,j=0;i<slen;){
            if (s[i] == mt[j]){
                i++;j++;
                if (j == mtlen){
                    answer++;
                    j = nxt[j-1]+1;
                }
            }
            else{
                if (j==0) {i++;}
                else {j = nxt[j-1]+1;}
            }
        }
        cout << answer << endl;
    }
    

讲完啦!下面是完整代码:

#include <bits/stdc++.h>
using namespace std;

const int Max_n = 1e5 + 10;
char s[Max_n], mt[Max_n];
int nxt[Max_n];

void init(){
    nxt[0] = -1;
    int mtlen = strlen(mt);
    for (int i=1;i<mtlen;i++){
        int temp = nxt[i-1];
        while(mt[temp+1] != mt[i] && temp > -1){
            temp = nxt[temp];
        }
        if (mt[temp+1]==mt[i]) nxt[i] = temp + 1;
        else nxt[i] = -1;
    }
}

void solve(){
    int slen = strlen(s), mtlen = strlen(mt), answer = 0;
    for (int i=0,j=0;i<slen;){
        if (s[i] == mt[j]){
            i++;j++;
            if (j == mtlen){
                answer++;
                j = nxt[j-1]+1;
            }
        }
        else{
            if (j==0) {i++;}
            else {j = nxt[j-1]+1;}
        }
    }
    cout << answer << endl;
}

int main(void){
    cin >> s >> mt;
    init();
    solve();
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值