KMP算法

一、引言

主串(被扫描的串):S=‘s0s1...sn-1’,i 为主串下标指针,指示每回合匹配过程中主串的当前被比较字符;

模式串(需要在主串中寻找的串):P=‘p0p1...pm-1’,j 为模式串下标指针,指示每回合匹配过程中模式串的当前被比较字符。

字符串匹配:在主串中扫描与模式串完全相同的部分,并返回其在主串中的位置,这里的起始扫描位置默认为主串的第一个字符,即默认pos=0,其他情况类似。

朴素匹配算法:在模式串与主串的匹配过程中,一共要进行n=Length(S)回合的匹配,每一回合分别从主串的起始字符s0,s1,...,sn-1开始进行。在具体某一回合的匹配过程中,每当模式串P中的某一字符与主串S中的被比较字符不相等,主串S的指针 i 都必须回溯到此回合起始字符的下一个位置,模式串P的指针 j 回到模式串串首,重新进行下一回合匹配。算法最坏情况下的时间复杂度为O(m*n)。这里不再详述。

KMP匹配算法:KMP是一个高效的字符串匹配算法,它是由三位计算机学者 D.E.Knuth 与 V.R.Pratt 和 J.H.Morris 同时发现的,因此人们通常简称它为 KMP 算法。在KMP匹配过程中,每当模式串P中的某一字符与主串S中的被比较字符不相等,主串S的指针 i 不需要回溯,而只要将P串“向右滑动”到一定位置,继续进行下一回合的比较。KMP算法的时间复杂度为O(m+n)。

下面主要理解KMP匹配算法。

1)先由KMP算法的主要思想得到next函数的定义

2)然后根据next函数定义求取next函数值

3)最后根据next函数值进行主串、模式串匹配

附:根据next函数定义一眼看出来next函数值。

 

二、定义next[j]

我们要解决的关键问题是:当本回合匹配过程中出现失配时,下回合匹配时模式串“向右滑动”的可行距离有多远,也即:本回合匹配过程中,主串S的第 i 个字符与模式串P中的第j个字符失配时,下回合匹配时主串中的第i个字符应与模式串中的哪个字符进行比较(设为next[j],因为是“向右滑动”,故next[j]<j)

引例:假设有以下主串和模式串:

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P: a b a a b c a c              
j: 0 1 2 3 4 5 6 7              

a)。。。之前匹配步骤

b)在经过若干回合匹配之后,两字符串状态如下(i=1,j=0):

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:   a  b a a b c a c            
j: -1  0 1 2 3 4 5 6 7            

c)此时主串的第1个字符与模式串的第0个字符不等,主串的指针i不回溯,由于next[j]<j,考虑定义:next[j] =next[0]= -1。模式串指针j向右移动至位置next[0]= -1,情形如下。(位置"j=-1"是假想的,在后面我们将会发现这样处理的好处)

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:     a  b a a b c a c          
j:   -1 0 1 2 3 4 5 6 7          

d)此时,模式串再也无法向右滑动,此轮匹配失败,令i和j均自增,继续进行比较,情形如下(i=2,j=0):

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:     a  b a a b c a c          
j:   -1 0 1 2 3 4 5 6 7          

e)此时字符相等,主串的指针i和模式串的指针j均自增1,继续匹配结果如下(i=3,j=1):

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:     a  b a a b c a c          
j:     0 1 2 3 4 5 6 7          

f)。。。如此重复e)

g)当i=7,j=5时,主串的与模式串当前字符不相等。情形如下(i=7,j=5):

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:     a  b a a b c a c          
j:     0 1 2 3 4 5 6 7          

此时,主串的指针i不回溯,那么模式串将向右滑动至什么位置呢?

我们假设next[j]=k(k<j)。

①本回合,si与pj失配(i=7,j=5),而在失配之前有“部分匹配”,故有:'p0p1...pj-1'='si-jsi-j-1...si-1',又因为k<j,从而'p0p1...pj-1'的部分串'pj-kpj-k+1...pj-1'满足:'pj-kpj-k+1...pj-1'='si-ksi-k+1...si-1';

②下回合,因为要保证从模式串的k位置处字符开始比较,那么必须保证模式串k位置之前的部分串'p0p1...pk-1'满足:'p0p1...pk-1'='si-ksi-k+1...si-1',其中k取最大的可能值;

由①式、②式右半边相等可知,k的取值满足下面的判别等式:'pj-kpj-k+1...pj-1'='p0p1...pk-1',其中k取该解集中的尽可能大值。考虑定义:next[j] = Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}。

上式也即:j位置之前的P尾串 = 0位置(包括0)之后k位置之前的P头串,如上图中:'p3p4'='p0p1'故k=2,它反映了模式串“在j位置之前的P尾串”与“0位置(包括0)之后k位置之前的P头串”的重复程度。

h)模式串向右滑动至位置next[j]=2,情形如下(i=7,j=2):

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:           a  b a a b c a c    
j:           0 1 2 3 4 5 6 7    

j)。。。后续匹配

根据以上讨论,对k(next[j])考虑如下定义:

(1)next[j] = -1(j=0);

(2)next[j] = Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}(j!=0且集合有解);

(3)next[j] = 0(j!=0且集合无解);

 

三、求取next[j]

根据next[j]的定义可知,此函数的值取决于模式串的本身以及模式串的失配位置。此时把求解next[j]问题看成是一个模式串自匹配问题即主串和模式串都是P,从主串P的p1开始与模式串P的p0开始进行匹配

当j=0时,由定义得:next[0]=-1;

当j!=0时,我们思路是:按照主串的下标j由小及大,依次求next[1]、next[2]、...、next[m-1],m为模式串的维数。现在假设已知next[j]=k,且next[t](t<j)均已求得。如果求得next[j+1]与next[j]的关系,那么所有的next函数值均可被求出。

此时,由next[j]的定义可知:'pj-kpj-k+1...pj-1'='p0p1...pk-1',且k为最大值,下面分两种情况讨论:

(a)如果pj=pk,结合'pj-kpj-k+1...pj-1'='p0p1...pk-1'可以得到'pj-kpj-k+1...pj-1pj'='p0p1...pk-1pk',又不存在k'>k满足该式,由next函数的定义可知:next[j+1]=k+1,也即:next[j+1]=next[j]+1。这个式子的意味着,该情况下主串字符指针(j+1)位置处的next[j+1]可以由当前j位置处的next[j]加1求得。(由于下标最小的next函数值next[1]=0是已知的,这使得按下标由小及大的顺序求解所有next函数值成为可能,这种情况对应着下面伪代码的 if(P[i] == P[j])语句部分)

(b)如果pj!=pk,将模式串向右滑动至k'(k'=next[k]<k<j)位置,使得主串的pj字符与模式串的pk'字符比较。

①如果此时pj=pk'(k'<k),结合'pj-kpj-k+1...pj-1'='p0p1...pk-1',则有'pj-k'pj-k'+1...pj-1pj'='p0p1...pk'-1pk'',由next函数的定义该式等价于:next[j+1]=k'+1=next[k]+1(观察下标k<j,由于next[t](t<=j)均为已知,则一定可以求出next[j+1])。

②如果此时pj!=pk',则将模式串继续向右滑动,直至pj和模式串的某个字符pk_lucky匹配成功,此时pj=pk_lucky(k_lucky<k),结合'pj-kpj-k+1...pj-1'='p0p1...pk-1',则有'pj-k_luckypj-k_lucky+1...pj'='p0p1...pk_lucky',由next函数的定义该式等价于:next[j+1]=k_lucky+1=next[...next[k]...]+1(在几次连续的滑动过程中,每次迭代k'=next[k],k'<k<j恒成立,由于next[t](t<=j)可知已知,则一定可以求出next[j+1])。

①和②的讨论说明,无论经过多少次滑动,只要主串的pj最终与模式串pk_lucky字符匹配成功,则主串字符指针(j+1)位置处的next[j+1]一定可以由next[t](其中t<=j)加1求得(这种情况对应着下面伪代码的else语句部分)

③尽管向右滑动,一直到j=next[t]=-1,很不幸找不到k'使得pj=pk',这相当于匹配过程中无解,此时由定义知next[j+1]=0(这种情况对应着下面伪代码的if(j==-1)部分)

故伪代码如下:

void get_next(SString P,  int next[]){
    //求模式串P的next函数值并存入数组next中,i、j分别代表主串、模式串的下标
    i = 0; j = -1; next[0] = -1;
    while(i < len(P)-1){
        if( j==-1 || P[i] == P[j] ) { ++i; ++j; next[i] = j; }//每当自增i和j,得到一个新的next[i]
        else j = next[j];//模式串向右移动
    }
}

例子:有以下模式串:
P: a b a a b c a c
j: 0 1 2 3 4 5 6 7

算法的前几次迭代过程列举如下:

a)执行if,++i,++j,进入初始状态(i=1,j=0),next[1]=0:

i: 0 1 2 3 4 5 6 7  
P: a b a a b c a c  
P:   a b a a b c a c
j: -1 0 1 2 3 4 5 6 7

b)执行else,模式串向右滑动(i=1,j=next[0]=-1):

i: 0 1 2 3 4 5 6 7    
P: a b a a b c a c    
P:     a b a a b c a c
j:   -1 0 1 2 3 4 5 6 7

c)j==-1,执行if,++i,++j(i=2,j=0),next[2]=0:

i: 0 1 2 3 4 5 6 7    
P: a b a a b c a c    
P:     a b a a b c a c
j:   -1 0 1 2 3 4 5 6 7

d)pi=pj,执行if,++i,++j(i=3,j=1),next[3]=1:

i: 0 1 2 3 4 5 6 7    
P: a b a a b c a c    
P:     a b a a b c a c
j:   -1 0 1 2 3 4 5 6 7

e)执行else,模式串向右滑动(i=3,j=next[1]=0):

i: 0 1 2 3 4 5 6 7      
P: a b a a b c a c      
P:       a b a a b c a c
j:     -1 0 1 2 3 4 5 6 7

以此类推。。。

 最后求得next[j]如下:

j: 0 1 2 3 4 5 6 7
P: a b a a b c a c
next[j] -1 0 0 1 1 2 0 1

 

四、改进的next函数值算法

这样的改进已经是很不错了,但算法还可以改进,注意到下面的匹配情况:

i 0 1 2 3 4 5 6 7 8
S a a a b a a a a b
P a a a a b        
j 0 1 2 3 4        
next[j] -1 0 1 2 3        
nextval[j] -1 -1 -1 -1 3        

模式串P中的p3='a'和主串S中的s3='b'失配时,P向右滑动至next[3]=2位置,由于p2=p3='a',此时的比较还是会失配;然后P向右滑动至next[2]=1位置,由于p1=p2='a',此时的比较还是会失配;P向右滑动至next[1]=0位置,由于p0=p1='a',此时的比较还是会失配,P向右滑动至next[0]=-1位置。

如果我们能够修正next[1]=next[next[1]]=next[0]=-1,修正next[2]=next[next[2]]=next[1](修正后的值)=-1,修正next[3]=next[next[3]]=next[2](修正后的值)=-1,使得出现类情况的时候,P都能够一次性直接滑动到next[0]=-1的位置,这相当于我们在求next函数值的时候,把这些冗余的比较进行预处理,如此就可以消除模式串与主串之间这样的多余比较。

设主串中的si和pj失配,按原定义模式串中应滑动到位置k=next[j],进行si和pk的匹配,而模式串中pk满足pk=pj,因此si不需要再和pk进行比较,而直接和pnext[k]匹配,也就是说此时的next[j]=next[k]=next[next[j]]。

i 0 1 2 3 4  
P a a a a b  
P   a a a a b
j   0 1 2 3 4
next[i] -1 0 1 2 3  
nextval[i] -1 -1 -1 -1 3  

对上面的get_next函数稍加改进得到:

void get_nextval(SString P,  int nextval[]){
    //求模式串P的next函数修正值并存入数组nextval
    i = 0; j = -1; nextval[0] = -1;
    while(i < len(P)-1){
        if(j == -1 || P[i] == P[j]){
            ++i; ++j;
            if(P[i] != P[j]) nextval[i] = j;//
            else nextval[i] = nextval[j];//nextval[i]=j意味着当主串的当前字符与模式串中的pi不匹配时,应与模式串的pj比较。而当pj=pi时,所以应该与模式串的pnext[j]比较。
     }
    else j=nextval[j];
  }
}

 

五、KMP算法

求得next[j]的函数值之后,我们就可以根据KMP算法进行字符串匹配了。

在匹配过程中,如果si=pj,则i和j分别增1;

否则,i不变,j移动到next[j]的位置继续比较,以此类推,直至下面两种可能:

1)j退至某个next值(next[next[…next[j]...]])时字符比较相等,则指针各自增1,继续进行匹配;

2)退到next[next[…next[j]...]]为-1,此时需要将模式串向右滑动一个位置,即从主串的下一个字符si+1起和模式串p0开始重新匹配。

KMP算法伪代码如下:

int Index_KMP(SString S, SString P, int pos){  
    //利用模式串P的next函数求P在主串S中第pos个字符之后的位置的KMP算法。
    //其中,P非空,0<=pos<StrLength(S)。  
    i = pos; j = 0;  
    while(i < len(S) && j < len(P)){
        if(j == -1 || S[i] == P[j]) { ++i; ++j; }//继续比较后继字符
        else   j = next[j];//模式串向右滑动
    }  
    if(j == len(P))   return  i - len(P);//匹配成功  
    else   return  0;  
}//Index_KMP

 

六、直接看出next[j]函数值(这部分可以用,不过原理不太清晰)

根据以上next[j]的定义:

(1)next[j] = -1(j=0);

(2)next[j] = Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}(j!=0且集合有解);

(3)next[j] = 0(j!=0且集合无解);

可以直接看出模式串的next函数值。这主要遵循2条规则

a)如果上一个next[j]!=-1,next[j+1]=Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'};

b)如果上一个next[j]=-1,由next[j]的求解算法知,此时主串第j位与模式串第0位对齐,比较这两位:①如果不等,则主串的第j+1位与模式串的第0位对齐,故next[j+1]=0;②如果相等,则next[j+1]=Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}

  

这篇文章是我理解KMP的思路,欢迎批评指正。

参考文献:《数据结构》,严蔚敏著

posted @ 2011-12-17 20:48  杨小明  阅读(6002)  评论(7编辑  收藏  举报