KMP算法从陌生到熟悉

目录

题目分析

解题过程


        一年之前学的KMP,那时候对着课件上的PPT和网课视频一直在发呆,用手不停地在模拟代码的实现过程,反复地让自己“被动理解”这个算法,结果很明显,假装懂了终究还是欺骗自己。把课件上的代码原封不动地提交一下,这件事让我知道昨天还自信满满地以为我已经掌握了KMP算法。但是这次不一样,经过HDU和LeetCode的捶打之后,思路突然就打开了,写篇博客纪念一下。 

题目分析: 

        实际上KMP算法就是去匹配一个文本串S中的某个给定模式串P(子串),暴力地去匹配,很简单,复杂度也就跟着上来了。KMP则是把暴力法中不必要的匹配过程压缩掉。

        比如我们要匹配 " abce abce abcf " 中的 " abce abcf "(注意我的空格是为了方便观察特点):

1  2  3  4  5  6  7  8  9  101112
abceabceabcf
abceabcf

 

        

         两个串匹配到第八位,出现不同字符,暴力地去匹配,应当是模式串右移一为,在从头和新一位开始比较:

        

        第一位就不匹配,再来:

        

        反复移动一直到这样:

        

       匹配成功了!我们一共移动了四次模式串。但是你会发现从第一步开始,后面的所有步都是傻子行为,因为在第一步中,我们已经知道文本串中的前7位和模式串已经匹配上了,再右移一位极有可能会打乱这已经匹配好的7位公有部分(例子有点特殊,可以简化思考过程),那么这部分的移动都是没有意义的了。仔细观察模式串 " abce abcf ",它包含了两个重复的部分 " abc ",我们可以用这个部分来简化移动。

        我用i来代表当前文本串S的匹配位数,j来代表模式串P的匹配位数。暴力匹配下,每一次i和j都要回退到一个起点来从头匹配。我们希望减少回退的位数或者尽量不回退,这里我们选择不让i去回退,利用模式串中的公共部分只进行j的回退。

        在第一步中,两个串中的5~7位都是 " abc ",而在模式串中,abc曾经在之前的某个位置出现过,那么下一次匹配的时候,我可以直接把曾经出现过的那个abc直接与文本串对齐,因为在这两个状态中间的所有状态都会破坏abc这个前缀的相对位置造成错位,不可能使两个串匹配。

        那么体现在刚才的模拟过程,我们只需要移动两次就可以找到P在S中的位置了,只不过我们需要额外保存P中公共部分的下标的相对关系,以便于匹配不成功,我该把j回退到哪个位置,也就是当前位置字符,在P中的上一个前缀能回退到哪个位置。比方说在 " abce abcf "中,f的前缀abc(5~7)可以回退到(1~3)的位置,我们就可以把(1~3)的那个abc拿过来与文本串的位置匹配,也就是下面这个过程:

        

        

        那么,我们接下来的任务就是统计模式串中的公共部分的下标(即找到j回退的位置),利用这个关系来进行KMP快速匹配。

解题过程:

        我们用一个辅助数组f [ ]来记录j的回退位置,对于刚才的例子," abce abcf " 对应的f数组如下(为了拟合字符串的存储规则我令下标从0开始):

j01234567
P[j]abceabcf
f[j]00001230

        

        解释一下,假如到 j = 7 不匹配了,那么我们就找到j的前一位第6位。f [ 6 ] = 3,那么我们就让j回退到3去和S [ i ]匹配,看一下是不是和上面举得例子贴合上了?下面我给出求解f数组的代码:

int f[MAXN];
void fail(string P)
{
    f[0] = 0; //初始化
    for (int i = 1, j = 0; i < m; i++) 
    {
        while (j > 0 && P[i] != P[j])
        {
            j = f[j - 1];
        }
        if (P[i] == P[j]) 
        {
            j++;
        }
        f[i] = j;
    }
}

        编码方式不唯一,递归也可以实现,小细节大家可以自行思考。

KMP匹配:

/**
    返回P在S中第一次出现的下标
*/
int KMP(string S, string P)
{
    int n = S.size();
    int m = P.size();
    for (int i = 0, j = 0; i < n; i++) 
    {
        while (j > 0 && S[i] != P[j]) //本位不匹配,利用f去回退直到公共前缀的下一位匹配成功
        {
            j = f[j - 1];
        }
        if (S[i] == P[j]) 
        {
            j++;
        }
        if (j == m) //j匹配到P的最后一位了,说明匹配成功,返回位置 
        {
            return i - m + 1;
        }
    }
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落英S神剑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值