KMP算法的理解和实现

想学会“看毛片”算法,今天和同学捣鼓了半天,终于理解并自己实现了它。主要参考的博客是http://blog.csdn.net/yutianzuijin/article/details/11954939/ 。由于自己实现了一遍KMP代码,为了加深印象和加强理解,写篇博客记录一下,欢迎讨论。

首先解释一下几个基本的名词:

1.模式串:用于匹配的子串
2.主串:可能包含模式串的字符串
3.失配:匹配失败

比如:

串A:"abcdefgabcde"
串B:"defg"

判断串A中是否包含串B,则串B为模式串,串A为主串

KMP算法的主要目的是判断模式串是否存在于主串中,若存在,返回匹配位置的下标

KMP算法与朴素的字符串匹配算法的最大区别就是:

朴素的字符串匹配算法是每次失配后将模式串向右移一个字符,继续从模式串首开始比较,这样做缺点是没有利用已经匹配的字符的信息,降低了效率;

KMP算法利用了已经匹配了的字符的信息,一次失配后尽可能的右移更多的字符,但又保证不会漏掉可能的匹配串,从而在保证精确的前提下提高了效率。

举个例子:

主串A: "abccdabcbc"
匹配串B:"abcd"

朴素匹配方法:

    前三次比较"abc"都匹配成功,第四次由于"c"和"d"不相同,所以失配;

    这时将子串右移一位,继续比较:

        abccdabcbc
         abcd

    发现第一位"a"和"b"失配,于是继续右移:
        abccdabcbc
          abcd
    .....

KMP的匹配方法:

    同样前三次比较发现"abc"匹配成功,第四位"d"失配;

    这时KMP算法将会利用“已经匹配了abc这3个字符”这个信息,显然

        a|bc|       |a|bc
         |ab|c    ab|c|

    都是不匹配的,所以这时可以直接将模式串右移3位,从一开始失配时的这样:

        abccdabcbc
        abcd

    变成这样:

        abccdabcbc
           abcd

经过观察和思考,KMP的发明者们发现,对于每一个已经匹配的长度(如上例中已经匹配了3个字符),都可以计算出一个“最大右移值”,且这个“最大右移值”仅与已经匹配的字符串有关(注意到上面的例子中我计算右移值为3位仅仅用到了’abc’这个已经匹配的字符串)

所以,对于每一个给定的模式串,我们都可以对它的每一个长度的匹配长度计算相应的“最大右移值”,在匹配主串时,对于每一个匹配长度,都按这个提前计算出的“最大右移值”进行右移,这就是KMP算法的基本思路。

那么,根据已经匹配的字符串,如何计算出所谓的“最大右移长度”呢?

这里引入“前缀”和“后缀”的概念:

还是上面的例子,“abc”是“abcd”的一个前缀,“ab”也是“abc”的一个前缀,“a”也是...

而“bcd”、“cd”、“d”都是“abcd”的后缀

但是"abcd"不是"abcd"的前缀和后缀,因为这样的前后缀对KMP算法是没有意义的。

当一个字符串的前缀和后缀完全相同时,我们记相同部分的长度为len,当len最大时,用这个字符串的长度减去l,就是我们的”最大右移值”

在确保自己理解了上面那句话之后,再看下面的内容就比较容易了。

KMP算法使用一个next数组来存储上面提到的len。

注意!注意!注意!重要的事情:

next数组的下标表示的是已经匹配的字符长度,是长度!由于匹配长度为0时KMP和朴素匹配方法的处理方法是一样的),所以next数组的下标从1开始。而匹配串的下标是从0开始的。

这点很重要,我和同学看了好多博客,发现看得越多越晕,很大的原因就是很多网上的代码没有交代清楚next数组的下标和匹配串下标的区别,导致代码总是看不懂….下面贴代码的时候我还会强调这个问题。

我们知道,对于每一个匹配长度,都可以计算出一个len值,比如:

模式串"abcbcabc"

的next数组中的值如下,下标从1开始:

next数组中len的值:[x,0,0,0,0,0,1,2,3]
对应next下标值   : 0,1,2,3,4,5,6,7,8

那么,如何用代码计算next数组呢?

下面是我写的代码:

#pragma once
#include<stdlib.h>
#include<iostream>
using namespace std;
#define MAXSIZE 100
#define PSIZE 9
void calcNext(int * next,char*pattern,int psize){

    //参数解析:next为存放计算之后的next数组
    //pattern为模式串
    //psize为模式串长度

    next[1] = 0;//匹配长度为1时,len必为0

    for (int i = 1; i < psize; i++){

        int n = next[i];

        while (n != 0 && pattern[n] != pattern[i]){
            //这里就体现了上文说的下标问题。注意这里做的工作是:
            //比较模式串的第next[i] + 1个元素和模式串的第i + 1个元素是否相等
            //next[i]==0时,则比较pattern的第一个元素和模式串的第i + 1个元素
            //不相等则比较模式串的第next[next[i]]+1个元素和第i+1个元素是否相等
            //如此迭代直到两者相等或next[n]为0

            n = next[n];
        }

        if (n == 0){
            if (pattern[n] == pattern[i]) next[i + 1] = 1;
            //如果只有模式串的第一个元素和第i+1个元素相同,则next[i+1] = 1.
            else next[i + 1] = 0;
        }
        else next[i + 1] = n + 1;
    }
}

从代码中可以看到,next[i+1]是可以通过next[i]迭代计算得到的,这么做的原理,篇首提到的那个参考链接中讲的很清楚,这里不再赘述。

next数组计算结束了,我们开始使用它来高效地检测字串,下面是我的测试代码:

int main(){
    char pattern[PSIZE + 1] = { 0 };
    int next[PSIZE + 1] = { 0 };//next[0]不用
    char buffer[MAXSIZE] = { 0 };
    int j = 0;
    cout << "Input the pattern string :(less than 9)" << endl;
    cin >> pattern;
    cout << "Input the string to match substring:" << pattern << endl;
    cin >> buffer;

    //计算next
    calcNext(next, pattern, PSIZE);

    //开始匹配,匹配方法和计算next数组神似,大家好好体会
    for (int i = 0; i < strlen(buffer); i++){
        while (j > 0 && pattern[j] != buffer[i]) j = next[j];
        if (pattern[j] && buffer[i] == pattern[j]){
            j++;
        }
        if(!pattern[j]){
            cout << "find substring at index: " << i - strlen(pattern) + 1 << endl;
            j = next[j];
        }
    }

    cout << "match over" << endl;
    system("pause");
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值