KMP算法分析

文中的公式图片在chrome浏览器下显示不正常,IE则显示正常。

 

一、引言

主串(被扫描的串):S=clip_image002[15],i 为主串下标指针,指示每回合匹配过程中主串的当前被比较字符;

模式串(需要在主串中寻找的串):P=clip_image004[15],j 为模式串下标指针,指示每回合匹配过程中模式串的当前被比较字符。

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

朴素匹配算法:在模式串与主串的匹配过程中,一共要进行n=Length(S)回合的匹配,每一回合分别从主串的起始字符clip_image006[14]clip_image008[14]、...、clip_image010[14]开始进行。在具体某一回合的匹配过程中,每当模式串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函数值进行主串、模式串匹配

4)文章最后,根据next函数定义一眼看出来next函数值(有待考证)。

 

二、定义next[j]

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

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

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

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

2

3

4

5

6

7

8

             

a)。。。之前匹配步骤

b)在经过若干回合匹配之后,两字符串状态如下:

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

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

2

3

4

5

6

7

8

           

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

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

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

8

         

d)令i和j均自增,继续进行比较,情形如下:

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

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

2

3

4

5

6

7

8

         

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

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

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

2

3

4

5

6

7

8

         

f)。。。如此重复e)

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

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

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

2

3

4

5

6

7

8

         

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

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

①本回合,clip_image012[14]clip_image014[62]失配(i=8,j=6),而在失配之前有“部分匹配”,故有:clip_image016[14],又因为k<j,从而clip_image018[74]的部分串clip_image020[20]满足:clip_image022[74]

②下回合,因为要保证从模式串的k位置处字符开始比较而不回溯,那么必须保证模式串k位置之前的部分串clip_image024[20]满足:clip_image026[20],其中k取最大的可能值;

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

上式也即:j位置之前的P尾串 = 1位置之后的P头串,如图中:clip_image030[14],k=3,反映了模式串在j位置之前的P尾串 与1位置之后的 P头串的重复程度。

f)模式串向右滑动至位置next[j]=3,情形如下:

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

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

2

3

4

5

6

7

8

   

h)。。。后续匹配

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

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

(2)next[j] = Max{k|1<k<j且clip_image028[76]}(j!=1且集合有解);

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

 

三、求取next[j]

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

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

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

此时,由next[j]的定义可知:clip_image028[77],且k为最大值,下面分两种情况讨论:

(a)如果clip_image036[20],结合clip_image028[78]}可以得到clip_image038[20],又不存在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 语句部分)

(b)如果clip_image040[14],将模式串向右滑动至k'(k'=next[k]<k<j)位置,使得主串的clip_image014[63]字符与模式串的clip_image042[26]字符比较。

①如果此时clip_image044[14](k'<k),结合clip_image028[79],则有clip_image046[50],由next函数的定义该式等价于:next[j+1]=k'+1=next[k]+1(观察下标k<j,由于next[t](t<=j)已知,则一定可以求出next[j+1])。

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

①和②的讨论说明,无论经过多少次滑动,只要主串的clip_image014[65]最终与模式串clip_image050[15]字符匹配成功,则主串字符指针(j+1)位置处的next[j+1]一定可以由某一个next[t](t<j)加1求得。

③尽管向右滑动,很不幸找不到k'使得clip_image044[15],这相当于匹配过程中无解,此时由定义知next[j+1]=1。

故伪代码如下:

void get_next(SString P,  int &next[]){

    //求模式串P的next函数值并存入数组next中,i、j分别代表主串、模式串的下标

    i = 1; j = 0; next[1] = 0;

    while(i < P[0]){

        if( j ==0 || P[i] == P[j] ) { ++i; ++j; next[i] = j; }//每当自增i和j,得到一个新的next[i]

        else j = next[j];//模式串向右移动

    }

}
 
  

有以下模式串:

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

 

P:

a

c

a

b

a

a

b

a

a

b

c

a

c

a

a

 

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

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

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

 

S:

a

c

a

b

a

a

b

a

a

b

c

a

c

a

a

 

P:

 

a

c

a

b

a

a

b

a

a

b

c

a

c

a

a

j:

0

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

b)执行else,模式串向右滑动(i=2,j=0):

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

   

S:

a

c

a

b

a

a

b

a

a

b

c

a

c

a

a

   

P:

   

a

c

a

b

a

a

b

a

a

b

c

a

c

a

a

j:

 

0

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

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

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

   

S:

a

c

a

b

a

a

b

a

a

b

c

a

c

a

a

   

P:

   

a

c

a

b

a

a

b

a

a

b

c

a

c

a

a

j:

 

0

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

d)clip_image056[26],执行if,++i,++j(i=4,j=2):

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

   

S:

a

c

a

b

a

a

b

a

a

b

c

a

c

a

a

   

P:

   

a

c

a

b

a

a

b

a

a

b

c

a

c

a

a

j:

 

0

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

以此类推。。。

 

四、改进的next函数值算法

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

i

1

2

3

4

5

6

7

8

9

 

S

a

a

a

b

a

a

a

a

b

 

P

a

a

a

a

b

         

j

1

2

3

4

5

         

next[j]

0

1

2

3

4

         

nextval[j]

0

0

0

0

4

         

模式串P中的clip_image058[14]和主串S中的clip_image060[20]失配时,P向右滑动至next[4]=3位置,此时的比较还是会失配,然后P向右滑动至next[3]=2位置,此时的比较还是会失配,P向右滑动至next[2]=1位置。如果我们能够修正next[4]的值为1,使得出现类情况的时候,P能够一次性直接滑动到next[4]=1的位置,如此便可以消除这样的冗余比较。

下面对next的函数值进行修正:如果我们事先知道,上一次主串模式串字符相等 clip_image062[14],经过++i,++j,再一次得到clip_image062[15],此时,i自增后的next[i]不取j,而是选取j的下一个状态next[j](由于j<i恒成立,next[j]的值均为已知的),这相当于我们在求next函数值的时候,把这些冗余的比较进行预处理,如此就可以消除模式串与主串之间这样的多余比较。

稍加改进得到:

void get_nextval(SString P,  int &nextval[]){

    //求模式串P的next函数修正值并存入数组nextval

    i = 1; j = 0; nextval[1] = 0;

    while(i < P[0]){

        if(j == 0 || P[i] == P[j]){

            ++i; ++j;

            if(P[i]! = P[j]) nextval[i] = j;//下次比较不相等,nextval[i]取j

            else nextval[i] = nextval[j];//下次比较仍然相等,nextval[i]取j的下一个nextval[j]

        }

        else j=nextval[j];

    }

} 

 

五、KMP算法

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

在匹配过程中,如果clip_image064[14],则i和j分别增1;否则,i不变,j移动到next[j]的位置继续比较,以此类推,直至下面两种可能:1)j退至某个next值(next[next[…next[j]...]])时字符比较相等,则指针各自增1,继续进行匹配;2)退到next[next[…next[j]...]]为0,此时需要将模式串向右滑动一个位置,即从主串的下一个字符clip_image066[8]起和模式串clip_image034[21]开始重新匹配。

KMP算法伪代码如下:

int Index_KMP(SString S, SString P, int pos){  

    //利用模式串P的next函数求P在主串S中第pos个字符之后的位置的KMP算法。

    //其中,P非空,1≤pos≤StrLength(S)。  

    i = pos; j = 1;  

    while(i <= S[0] && j <= P[0]){//S[0]以及P[0]代表字符串长  

        if(j == 0 || S[i] == P[j]) { ++i; ++j; }//继续比较后继字符

        else   j = next[j];//模式串象右滑动

    }  

    if(j > P[0])   return  i - P[0];//匹配成功  

    else   return  0;  

}//Index_KMP

以下主串和模式串的匹配过程如图所示:

i:

1

2

3

4

5

6

7

8

9

11

12

13

14

15

16

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

2

3

4

5

6

7

8

             

clip_image067

六、直接看出next[j]函数值(这部分有待考证)

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

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

(2)next[j] = Max{k|1<k<j且clip_image028[81]}(j!=1且集合有解);

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

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

a)如果上一个next[j]!=1,按照next[j] = Max{k|1<k<j且clip_image028[82]}的规则求解下一个next[j+1]的值;

b)如果上一个next[j]=1,由next[j]的求解算法知,此时主串的第j位与模式串的第1位对齐,比较这两位:如果不等,则主串的第j+1位与模式串的第1位再对齐,故next[j+1]=1;如果相等,则next[j+1]=Max{k|1<k<j且clip_image028[83]}

举例如下:

P

a

b

a

a

b

a

a

a

b

a

j

1

2

3

4

5

6

7

8

9

10

next[j]

0

1

1

2

2

3

4

5

2

3

由定义知next[1]=0;

由j!=1且集合无解知next[2]=1;

由于next[2]=1,此时i=2,j=1,令这两个字符对齐,发现这两个字符不相等,则next[3]=1;

由于next[3]=1,此时i=3,j=1,令这两个字符对齐,发现这两个字符相等,根据next[j+1]=Max{k|1<k<j且clip_image028[84]}=2,故next[4]=2;

P(1之后串头)

a

b

a

a

b

a

a

a

b

a

P(j之前串尾)

a

b

a

a

b

a

a

a

b

a

j

1

2

3

4

5

6

7

8

9

10

next[j]

     

2

           

由于有1个匹配,故next[5]=2;

P(1之后串头)

a

b

a

a

b

a

a

a

b

a

P(j之前串尾)

a

b

a

a

b

a

a

a

b

a

j

1

2

3

4

5

6

7

8

9

10

next[j]

       

2

         

由于有2个匹配,故next[6]=3;

P(1之后串头)

a

b

a

a

b

a

a

a

b

a

P(j之前串尾)

a

b

a

a

b

a

a

a

b

a

j

1

2

3

4

5

6

7

8

9

10

next[j]

         

3

       

。。。后续匹配

 

这个文章思路是我理解KMP的思路,内容可能显得比较啰嗦,但是很具体,希望对大家有所帮助。

第一次写文章,还请大家多多提意见。

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

你可能感兴趣的:(KMP)