kmp算法

来源:互联网 发布:mac如何彻底关闭程序 编辑:程序博客网 时间:2024/04/29 01:58

1. 朴素匹配算法

                   过程如图:
          
         
       
     
翻译成代码:
int index_bf(char *s, char *p){    if(s==NULL || p==NULL)        return -1;    int i=0,j=0;    while(s[i+j]!='\0' && p[j]!='\0')    {        if(s[i+j]==p[j])            j++;        else        {            j=0; i++;        }    }    if(p[j]=='\0')        return i;    else        return -1;}

2. kmp匹配过程

如图:
       
 1. 可以看到匹配过程中 i 跟 j 是同时增加的,不同于朴素匹配算法只增加 j 。 在 i=5, j=5的时候有c!=d,这个时候的处理过程如下图
      
2. 这里模式串被右移了3个位置,实际上就是j=5变成j=2(next[5]=2 为什么??)。i=5保持不变,继续匹配直到i: 5->8, j:2->5。上图绿色部分ab两个字符我们没有比较过,因为next[5]=2隐含了前面2个字符不需要在比较的意思。
     

3. next[j]=k的含义

              1. 直观的含义是::设置j=k, 即把模式串右移j-k个位置。 如:next[5]=2,表示j=2即模式串右移5-2=3个位置。
              2. 更进一步的含义是:模式串前面k的字符已经比较过,不需要再比较了,直接从j=k(因为下标从0开始)这个位置开始比较
              3. 再进一步的含义可以用下面的图表示(next[5]=2 ):我们发现了在k=2这个位置,把模式P[0-4]分成了3个部分 "P[0]P[1]"="P[3]P[4]"还有个就是P[2]这3个部分。
       
  举个列子上面的next[5]=2是因为:
       
               4. 用一个比较专业的术语来描述就是,在P[0~j-1]中找一个最长真前缀使得它等于P[0~j-1]的最长真后缀。

 这里最长的意思举个例子如下:虽然下图有2种方法把模式串分成3部分,但是我们取k=3
       

 4. 为什么一定是右移j-k

           但我们在 j 这个位置发现失配的时候(S[i]!=P[j]), 模式串可以右移1,2,..,j-1。那为什么一定是j-k呢。 下图可以帮助理解
     
我们有K={1,4}, 任何不属于K的移动,即右移1,2,3,5,7次,都会有部分模式串与S[....~i-1]不匹配。同时取k=1右移6次的话发现错过了匹配的机会,所以一定是右移j-k次。 具体证明略。

5. 求next

         再说下next[j]=k是把P[0]~P[j-1]分成了3个部分:
        
      但是2这个部分是可以为空的:
    
    如果2为空,1跟1相交也是可以的:
    
注:next[0]= -1, -1 表示S[i]与P[0]不需要比较了,直接比较S[i+1]跟P[0]. 
按照上面的规则:我们来看几个例子:


求next的算法,虽然我们很容易能看出next的值,但是如果从找最长相等的真前缀跟真后缀这个角度出发的话,求出next数组的复杂度可能比朴素匹配的复杂度还高。实际上的求法是一个迭代的过程。即: next[j] = f(next[j-1]), 因为next[0]=-1已知道,f为某种规则。

算法:
          1.  已知next[j]=k, 如果P[j]=P[k], 则next[j+1]=next[j]+1=k+1。原因如下图:


          2. 如果P[j]!=P[k]怎么办呢, 只要一直求一个k(k = next[k]), 使得p[k]=p[j], 或者直到k=-1。如图:
  

上诉过程很容易翻译成代码:
void get_next_tmp(char *p, int next[]){    int l=strlen(p);    int i=0,k=-1;    next[0]=-1;    while(i<l-1)    {        if(k==-1 || p[i]==p[k])        {            i++;k++;            next[i]=k;        }        else            k=next[k];    }}

6. 改进next

         上诉的next还可以再改进,能改进的原因其实也很简单。因为我们再求next[j]的时候只考虑了0~j-1串的情况,至于P[j]到底是什么字符完全没有考虑,实际上 j 这个位置的信息能让我们把模式串更加往右移一些。
 
        1. 先看图吧:

这个图的意思是这样的,首先P[j]!=S[i], 按照上述的规则算出来next[j]=k, 有P[k]=p[j]!=S[i], 所以完全没必要把j回溯到k,  我们继续求K' = next[k], 这时候有P[K']=S[i]='a'. 所以方法是:一直求k=next[k], 直到k=-1或者P[K]!=p[j], 实际上我们每一个next[j]都是这样 求出来的,所以只需要一步k=next[k]即可。

用上述的规则把前面的4个例子改下如下:

好了,其实代码也是很直观的:
void get_next(char *P, int n[]){    int i=0,k=-1;    int l=strlen(P);    n[0]=-1;    while(i<l-1)    {        if(k==-1 || P[i]==P[k]){            ++i;++k;            if(P[i]!=P[k])                n[i]=k;            else                n[i]=n[k];        }        else            k=n[k];    }}

7. kmp匹配程序

        最后kmp_index函数:
int kmp_index(char *s, char *p){    if(s==NULL || p==NULL) return -1;    int pl = strlen(p);    if(pl==0) return -1;    int *n=(int *)malloc(sizeof(int)*pl);    get_next(p,n);    int i=0,j=0;    while(s[i]!='\0' && p[j]!='\0')    {        if(s[i]==p[j])        {            i++;j++;        }else        {            if(n[j]!=-1){                j=n[j];            }else{                j=0;                ++i;            }        }    }    free(n);    if(p[j]=='\0')        return i-j;    else        return -1;}















原创粉丝点击