KMP 算法

来源:互联网 发布:羽绒服推荐 知乎 编辑:程序博客网 时间:2024/06/05 15:51

关于这个KMP算法,我研究了近一个周才有点明白,总之很复杂,看了很多资料,最受启发的还是youtube上的视频,其次是这里。现在记录下来。

我们以以如下例子说明

text:      ABCDABABCDABCABCDABY  (i<n)

pattern:ABCDABY (j<m)

 

Naive way:

首先说一下暴力方法,这个也是最基本的。不要忽略这一步,虽然原理简单,但是实现也是有技巧的。

我们用pattern的每一位去和text的每一位比较,如果遇到不匹配,则将text的索引位置向后加1.然后再每一位进行比较。

text:    ABCDABABCDABCABCDABY

pattern: ABCDABY

首先text的索引位置i为0,pattern的索引位置j为0.。如果text[i] == pattern[j] 说明对应位置匹配,则i++,j++,比较下一位。如果遇到不匹配的位置,那么i需要退回到上一次开始的地方。

比如说,第一次是i=0开始比较,那么需要退回到i=1,重新比较。所以我们根据这个逻辑可以写出如下代码:

int naiveSearch(string &text,string &pattern){    int textLen = text.size();    int patternLen =  pattern.size();    int i=0,j=0;    while(i<textLen && j<patternLen)    {        if(text[i] == pattern[j])        {            i++;            j++;        }else{            i=i-j+1;            j=0;        }    }    if(j == patternLen)        return i-j;    else        return -1;}

 

我第一次根据逻辑写代码的时候,很自然想到了用for循环,但是因为要随时修改索引位置,也就是i,j的值,所以这里用while循环更合适。并且只要保证循环索引在对应数组范围内即可。这种暴力的计算方法,它的复杂度为O(n*m)。因为在最差情况下,pattern的每一位都需要和text的每一位进行比较。

 

KMP 算法:

Naive的方法中,我们不断退回i的值,然后重新比较Pattern。KMP算法中,i的值是不变的。如果遇到不匹配,则不断退回pattern的索引值j。就是说,其实最主要的是当遇到不匹配时,我们要知道pattern[j] 前面有多少个字符是和pattern从0开始的字符是重复的。举个例子。

pattern: ABCDABY 

我们比较到Y的时候发现不匹配,那么其实我们下一步接着比较C即可。因为C之前的AB和Y之前的AB相同。此时j=6,那么我们将j调整到2即可。pattern的每一位都对应一个位置,用来记录失配时,应该将j调整到哪里。用来记录这个位置的数组,就是我们常说的Next数组。

 

计算Next数组:

其实计算Next数组就是分析Pattern 中重复前缀后缀的过程。还是以ABCDABY为例: 

ABCDABY

我刚刚写了很多计算过程,想了想又删除了,因为对于知道这个计算逻辑的人来说,不需要我罗嗦,不知道计算逻辑的人,又会被我的罗嗦给弄晕。所以这里我直接给出逻辑。

  1. 令i=0,j=1。同时Next[0]=0。
  2. 判断Pattern[i] 是否等于Pattern[j],如果相等,则Next[i]=j+1,且i++,j++。
  3. 如果不相等,则Next[i]=j。再次判断j是否等于0,如果等于0,则i++。如果j大于0,则令j=Next[j-1],同时i++。

整个逻辑就是这样计算。我们看一下代码:

void kmpPreProcessing(string &pattern,int *p){    int j=0,i=1;    int len = pattern.size();    p[0]=0;    while(i<len)    {        if(j ==0)        {            if(pattern[i] == pattern[j])            {                p[i] = j+1;                i++;                j++;            }else            {                p[i] = j;                i++;            }        }else        {            if(pattern[i] == pattern[j])            {                p[i] = j+1;                j++;                i++;            }else            {                j = p[j-1];            }        }    }}

 

这里的数组p就是Next数组。至于里面的细节,我仔细考虑了一下,要么用数学证明,要么自己按照上面的逻辑自己算一遍,好好琢磨一下。否则真不太好理解。特别是为什么j=p[j-1]。 最后完整的例子如下:

#include <iostream>#include <string>using namespace std;int naiveSearch(string &text,string &pattern);void kmpPreProcessing(string &pattern,int *p);int kmpSearch(string text,string pattern,int *p);int main(){    cout << "Hello world!" << endl;    string pattern = "ABCDABD";    string text = "BBC ABCDAB ABCDABCDABDE";    int pos1 = naiveSearch(text,pattern);    cout << "pos1--->" << pos1 << endl;    int *p = new int[pattern.size()];    kmpPreProcessing(pattern,p);    int pos2 = kmpSearch(text,pattern,p);    cout << "pos2--->" << pos2 << endl;    delete [] p;    return 0;}int naiveSearch(string &text,string &pattern){    int textLen = text.size();    int patternLen =  pattern.size();    int i=0,j=0;    while(i<textLen && j<patternLen)    {        if(text[i] == pattern[j])        {            i++;            j++;        }else{            i=i-j+1;            j=0;        }    }    if(j == patternLen)        return i-j;    else        return -1;}int kmpSearch(string text,string pattern,int *p){    int i=0,j=0;    int textLen = text.size();    int patternLen = pattern.size();    while(i<textLen && j<patternLen)    {        if(text[i] == pattern[j])        {            i++;            j++;        }else        {            if(j==0)            {                i++;            }else            {                j = p[j-1];            }        }    }    if(j == patternLen)    {        i = i-j;        return i;    }    return -1;}void kmpPreProcessing(string &pattern,int *p){    int j=0,i=1;    int len = pattern.size();    p[0]=0;    while(i<len)    {        if(j ==0)        {            if(pattern[i] == pattern[j])            {                p[i] = j+1;                i++;                j++;            }else            {                p[i] = j;                i++;            }        }else        {            if(pattern[i] == pattern[j])            {                p[i] = j+1;                j++;                i++;            }else            {                j = p[j-1];            }        }    }}

  

这个算法是我用了近一周时间查资料分析出来的,和网上很多文章的代码不一样,其实逻辑都一样。我这个代码是我自己写出来,并且运行过。如果自己考虑的话,肯定有优化空间。最明显的,如果Pattern比Text都要长,这个问题就没做判断。运行结果就不贴了。KMP算法能将复杂度降低到O(m+n)。

最后说一下,KMP算法其实不常用,根据Robert Sedgewick的<<算法>> 第四版中说明,KMP算法适用于:在text是输入流的场景。因为i不会回溯。这样就不涉及到缓存问题。但如果一次性读入text到内存,那么比KMP快的算法还有其他的,下次再说。另外,KMP算法适用的是Pattern中有重复的字串。但很多应用场景下,这种Pattern其实是不常见的。但是为了研究算法,我这里还是仔细分析了一把。如果有问题,请各位留言,谢谢。

 

原创粉丝点击