KMP 算法

关于这个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其实是不常见的。但是为了研究算法,我这里还是仔细分析了一把。如果有问题,请各位留言,谢谢。

 

posted on 2017-08-24 15:39  ^~~^  阅读(731)  评论(0编辑  收藏  举报

导航