转帖 http://blog.csdn.net/a56508820/archive/2009/07/14/4346596.aspx

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 c 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;

}


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/a56508820/archive/2009/07/14/4346596.aspx
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值