KMP模式匹配

KMP模式匹配可以避免许多重复比较,而且可能一次把模式串向右移动多个位置,而不是只移动一个位置。这样在匹配过程中完全不需要回头检查正文串已经过去的东西。无回溯匹配算法对联机匹配工作特别适用,例如一个系统在计算中每次从外部得到正文的一个字符,依次进行匹配。除了外存文件信息的匹配问题就属于这一类。

 


基本概念:

模式匹配是对字符串的一种非常重要的操作,假设被匹配的正文字符串是text,模式串是pattern,则模式匹配的任务就是在text中找出所有的pattern,给出pattern在text中的位置。

例如:text是“cdghcdghhcdr”,pattern是“cd”,则答案是0,4,9(下标计数从0开始)

简单字符匹配:

       第1轮:

用pattern的第1个字符与text的第1个字符比较,不等就结束,相等就

用pattern的第2个字符与text的第2个字符比较,不等就结束,相等就……

第2轮:

用pattern的第1个字符与text的第2个字符比较,不等就结束,相等就

用pattern的第2个字符与text的第3个字符比较,不等就结束,相等就……

……

这种做法非常直观,实际上就是把模式串pattern放到正文text的各个位置逐一匹配,假设text、pattern的长度分别是m、n,则上述做法总共有m轮,每一轮比较n次,所以该算法的时间复杂性是O(mn)。

 

KMP模式匹配:

       简单匹配法在一次字符比较失败后,只是向前移动一个位置,丢掉了由前面已做过的字符匹配的信息,当模式串pattern后面部分与前面部分有重复(匹配)时,一次可以移动多个位置。

考虑如下例子:

表1

位置             0    1    2     3     4     5     6      7      8      9      10

正文字符串    c     a    g     a     c     a     g      a            a       g      a       t        a

模式                    a    g     a     c     a     g      a      t       a
   
 


模式pattern后面有aga(第4到第6)与最前面aga(第0到第2)重复(匹配)

位置0不匹配,从位置1开始,匹配在正文text的第8个位置失败,如表2:

表2

位置             0    1    2     3     4     5     6      7      8      9      10

正文字符串    c     a    g     a     c     a     g      a      c       a       g      a       t       a

模式                    a    g     a     c     a     g      a      t       a

模式移动                                        a     g      a      c       a        g      a      t       a

 

因为已经知道了pattern第4到第6个字符与最前面第0到第2个字符匹配,所以下面的匹配过程直接移动4个位置,并且用pattern的第3个字符与text的第8个字符开始比较,如表3。

表3

位置             0    1    2     3     4     5     6      7      8      9      10

正文字符串    c     a    g     a     c     a     g      a      c       a       g      a       t       a

模式移动                                        a     g      a      c       a        g      a      t       a   
 


实现kmp算法,需要一个临时数组存放支持匹配过程的有关信息,这个数组称为前缀数组,它是一个整数数组,大小等于模式串pattern的长度,前缀数组不妨定义为prefix[i],设prefix[i]==j,则prefix[i]表示从模式串pattern的第i个位置(包括i)开始倒数共j个字符,这段字符与pattern的最开始j个字符相同。prefix[i]称为前缀计数(也可以叫它匹配长度),它也可以认为从模式串pattern[i](包括pattern[i])开始倒数到pattern[1]的范围内可匹配的长度。注意,这个倒数的范围不能到第 0 个位置,否则所有的prefix[i]=i+1。如表4。

表4

位置 i           0    1    2     3     4     5     6      7      8      9      10     11     12     13      14       15

前缀 prefix[i]  0    0    0     0     1     2     3      1      2      3       4       5       6       7        4         0

模式             a    g    c      t     a     g     c       a     g       c        t       a       g       c         t         a

                                             a     g     c       t     a       g       c       a       g       c        t         a

                                                                   a      g       c       t       a       g       c         a         g

                                                                                                     a       g       c         t          a
 


例如,从表4可知,prefix[14]倒数的范围应该是从prefix[14]到prefix[1],逐一判断是否与最开始的1、2、3…14个字符相同,最终的结果是4,即prefix[14]==4,表示从位置14开始(包含14),倒数4个字符(即agct)与pattern最开始的4个字符相同。实际的处理中,无需判断14次那么多,我们后面的算法中可看出来。

前缀数组的作用是指出当前字符匹配出现失败时如何处理。例如在表4中当匹配在模式串pattern的第10个位置失败时,prefix[9]==3告诉我们在模式串pattern的第10个位置之前的匹配有3个字符(其实就是第7到第9)与最前面的3个字符匹配,因此可以从模式串pattern中的第4个字符(就是pattern[3],即t)开始比较下去。

 

下面重点考虑前缀数组prefix的初始化,这个乃是KMP的精华所在啊,很多人对kmp的不理解也就在这个地方。

首先,显然有prefix[0]=0;假设字符数组模式串pattern的长度为patlen,下面先给出代码1:

代码1:

prefix[0]=0;

for(int i=1; i<patlen; i++)

{

       int k=prefix[i-1];

       while( pattern[i] != pattern[k]  &&  k!=0 )

              k=prefix[k-1];

       if( pattern[i] == pattern[k])

              prefix[i]=k+1;

       else

              prefix[i]=0;

}

假设现在要设置prefix[i]的值,有两种情况:

情况1:对应字符互相匹配,prefix[i]的值可在prefix[i-1]的基础上加1;

情况2:对应字符不匹配,不能在原有基础上加1,需确定新的前缀计数。注意,新的前缀计数不一定是从0或1开始计数,例如表4中prefix[14]是从4开始新的前缀计数。

按照如上分析,貌似代码应该写成如下形式:

代码2:

prefix[0]=0;

for(int i=1; i<patlen; i++)

{

       int k=prefix[i-1];

       if( pattern[i] == pattern[k])

              prefix[i]=k+1;

       else

              其它处理;

}

 

例如表4中处理到prefix[14]时,发觉prefix[13]的前缀计数是7,就用pattern[14]与pattern[7](即pattern[k])进行比较,相同则prefix[14]=7+1,不同就做其它处理。这样写代码也可以,但是后面的“其它处理”同样有pattern[i] 与pattern[k]的比较,写起来反而复杂。

现在重点分析代码1,主要是里面的while循环较难理解。

while有2种走法:

第一种:一开始就有pattern[i] == pattern[k],也就是情况1里的对应字符互相匹配,然后流程就进入后面的if,前缀计数加1,这种情况最易理解。

第二种:一开始pattern[i] != pattern[k],表示前缀计数不能加1,需重新计数,令k=prefix[k-1],这里使用了一个技巧。

以表4中prefix[14]为例(此时i=14),prefix[13]==7表示前缀计数为7,或者说从pattern[13]开始倒数7个字符(包括pattern[13])与最开始的7个相同,即:

pattern[7]到pattern[13]  与

pattern[0]到pattern[6]   相同

下面就要判断

pattern[7]到pattern[14]  是否与

pattern[0]到pattern[7]   相同? 因为pattern[14]!=pattern[7],这表示,前缀计数为8是不可能的了。下面需确定新的前缀计数,不管新的前缀计数是多少,它都必须从pattern[14]开始倒数x个字符(包括pattern[14])来判断是否与最开始的x个字符相同。而x的取值范围并不是从1到13,也就是说并不需要从pattern[14]倒数到pattern[1]。要从pattern[14]开始倒数,也可以先看看pattern[13]开始倒数能有多少个字符与最开始的相同,只是此时倒数的极限是pattern[8](即只能数7-1=6个字符来判断,如果数到pattern[7],将变成prefix[13],那么又要判断前缀计数能否是8了),同时,我们发现

pattern[7]到pattern[13]  与

pattern[0]到pattern[6]   相同

从pattern[13]开始倒数到pattern[8]与从pattern[6]倒数到pattern[1]是等价的,而从pattern[6]倒数到pattern[1]的前缀计数prefix[6]早已求出。

因此:从pattern[14]开始倒数,等于从pattern[13]开始倒数prefix[13]-1个字符的前缀计数(暂记为p,这个前缀计数等于prefix[ prefix[13]-1 ],都是倒数prefix[13]-1个字符,早已求出),再比较pattern[14]与pattern[p],相同,退出while循环,那么pattern[14]的前缀计数就是p+1;如果不相同,那么while循环继续,倒数p-1个字符,递归处理。

下面总结一下并向代码1的符号靠拢:要计算prefix[i],先赋值k=prefix[i-1],当pattern[i] != pattern[k]时,表示prefix[i]要重新确定前缀计数,这个前缀计数等于从pattern[i-1]开始倒数k-1个字符(数到pattern[i-k+1])范围内所得的前缀计数(因为pattern[i-k]到pattern[i-1]刚好等于pattern[0] 到pattern[k-1],而从pattern[k-1]数到pattern[1]范围内的前缀计数早已求出,就是prefix[k-1]),重新赋值给k,然后再次比较pattern[i]与pattern[k],如果仍然不相等,则从pattern[i-1]开始倒数k-1个字符(此时的k是新的值),同理,前缀计数等于prefix[k-1],循环直至pattern[i]与pattern[k]相同或k==0;最后,总的前缀计数等于新的前缀计数再加上判断pattern[i]与pattern[k]的计数,相等则为k+1,不相等为k(此时k必为0)。

 

int k=prefix[i-1];// 先赋值k=prefix[i-1]

/

while( pattern[i] != pattern[k]  &&  k!=0 )// pattern[i] != pattern[k]时

              k=prefix[k-1];// 前缀计数等于…所得的前缀计数…就是prefix[k-1], 重新赋值给k

然后再次比较pattern[i]与pattern[k],如果仍不等,则从pattern[i-1]开始倒数k-1个字符(此时的k是新的值),同理,前缀计数等于prefix[k-1],循环直至pattern[i]与pattern[k]相同或k==0;

/    

//总的前缀计数等于新的前缀计数再加上判断pattern[i]与pattern[k]的计数

//相等则为k+1,不相等为k(此时k必为0)。

if( pattern[i] == pattern[k])

              prefix[i]=k+1;

       else

              prefix[i]=0;

 

写下这些,我基本上明白了,但就这么简单的几句代码,我绞尽脑汁却无法把它描述得更简单。事实上,精华就是一句话:假设前一个前缀计数是p,新的前缀计数是q,则q必然不能超过p,或者说,新的前缀必须是前一个前缀的前缀,因此,只需从前一个前缀的倒数位置(即当前位置的前一个位置)开始倒数p-1个字符来判断就可以了,而从前一个前缀的倒数位置开始倒数p-1个字符效果等同于从p-1位置开始倒数p-1个字符来判断,即q= prefix[p-1],天呀,这句精华怎么还是这么长?

再讲述一下表4的例子:

prefix[7]:因为prefix[6]=3,因此要判断前缀计数是否为4,即判断pattern[3]与pattern[7]是否相同,结果不同,于是从pattern[6]开始倒数prefix[6]-1=2个字符,即倒数pattern [6]、 pattern [5],它的效果与从pattern [prefix[3-1]]开始倒数2个字符,即倒数pattern [2]、 pattern [1]是一样的,而从pattern [prefix[3-1]]开始倒数的前缀计数就是prefix[3-1],这个是前面计算的结果,就是0,再判断pattern [0]与pattern[7],结果相同,于是prefix[7]=1;

prefix[14]:和上面类似,要判断前缀计数是否为8,先判断pattern[7]与pattern[14]不同,于是从pattern[13]开始倒数prefix[13]-1=6个字符,效果等同从pattern[6]开始倒数6个字符所得到的前缀计数,即prefix[6]=3,然后判断pattern[3]与pattern[14]相同,于是prefix[14]=4;

prefix[15]:要判断前缀计数是否为5,先判断pattern[4]与pattern[15]不同,于是从pattern[14]开始倒数prefix[14]-1=3个字符,效果等同从pattern[3]开始倒数3个字符所得到的前缀计数,即prefix[3]=0,然后判断pattern[0]与pattern[15]不同,此时代码1中的k==0,于是prefix[15]=0。

 

 

 

以下给出完整源代码:

#include <iostream>

using namespace std;

//求字符串长度

int CharLen(const char *Text)

{

     if( !Text )

         return -1; //空指针返回-1。

     int len=0;

     const char * c=Text;

     while(*c++!='/0')

     {    

         ++len;//字符串长度。

     }

     return len;

}

//设置前缀数组

void SetPrefix(const char *Pattern, int prefix[])

{

     int len=CharLen(Pattern);//模式字符串长度。

     prefix[0]=0;

     for(int i=1; i<len; i++)

     {

         int k=prefix[i-1];

         while( Pattern[i] != Pattern[k]  &&  k!=0 )

              k=prefix[k-1];

         if( Pattern[i] == Pattern[k])

              prefix[i]=k+1;

         else

              prefix[i]=0;

     }

}

//KMP模式匹配

int KMP(const char *Text,int pos,const char* Pattern)

{

     if( !Text||!Pattern|| Pattern[0]=='/0'||Text[0]=='/0' )

         return -1; //空指针或空串,返回-1。

    

     int Patternlen=CharLen(Pattern); //模式字符串长度。

     int *prefix=new int[Patternlen+1];

 

     SetPrefix(Pattern,prefix); //求Pattern的前缀数组

 

     int Textlen=CharLen(Text);

     int patp=0; //模式中的位置

     for(int i=pos;i<Textlen;i++)

     {

          while(patp>0 && Pattern[patp]!=Text[i])

              patp=prefix[patp-1] ; //第patp个位置与Text[i]不同,则可从prefix[patp-1]这个位置开始比较

                                 //而不需要直接从模式串的第个位置开始比较

         if(Pattern[patp]==Text[i]) //模式串与正文中的一个字符相匹配

         {

              //匹配长度加后判断是否等于模式串的长度,若等于则返回相应的位置

              if(++patp==Patternlen)

              {

                   return i-Patternlen+1;

              }

         }

     }

     delete []prefix;

     prefix=0;

     return -1;

}

 

 

int _tmain(int argc, _TCHAR* argv[])

{

     char *text="cabcdabcabcdaababcbaaabcdabcabcaabc";

    char *pattern="abcdabcab";

     int pos=-1;

     do

     {

         pos=KMP(text,pos+1,pattern);

         cout<<pos<<'/n'<<endl;

     }while(pos!=-1);

 

     return 0;

}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值