深入理解KMP算法

介绍

KMP算法是字符串匹配算法领域的一个经典算法, KnuthMorrisPratt三人提出,匹配复杂度是线性的,但是非常晦涩,初看不容易看明白,特别是next函数的计算.本文详细讲述KMP算法的具体原理和实现,并给出C++实现.

 

本文用到的标记

目标串: T[0,1,2….(n-1)] 长度为n

模式串: P[0,1,2….(m-1)] 长度为m

匹配失败时模式串序号返回的位置:next[0,1,2…m] 长度为m

 

内容

1.       朴素匹配算法

在讲述KMP之前,首先要理解朴素匹配是如何工作的,先看一个例子:

目标串:”abaababba”

模式串:”abab”

第一次匹配

序号

0

1

2

3

4

5

6

7

8

目标串

a

b

a

a

b

a

b

b

a

模式串

a

b

a

b

 

 

 

 

 

从这里看出,匹配到目标指针等于3时匹配失败.

朴素的算法是这样处理的:

当匹配失败时,目标指针回退到上一次开始比较的位置的下一位置,模式串指针直接回退到起点:如本例中,目标指针回退到1,模式串指针回到0,好,继续比较:

序号

0

1

2

3

4

5

6

7

8

目标串

a

b

a

a

b

a

b

b

a

模式串

 

a

b

a

b

 

 

 

 

 

就这样直到这里匹配成功

序号

0

1

2

3

4

5

6

7

8

目标串

a

b

a

a

b

a

b

b

a

模式串

 

 

 

a

b

a

b

 

 

 

2.       朴素算法的改进点

从上面朴素算法看,算法的确可以进行改进,为什么呢?我们看

第二次匹配

序号

0

1

2

3

4

5

6

7

8

目标串

a

b

a

a

b

a

b

b

A

模式串

 

a

b

a

b

 

 

 

 

 

这次匹配必须吗?

在上一次的匹配中,是在模式串的第3位才匹配失败的,那么我们知道

T[0,1,2] == P[0,1,2]  -----(1)

下一次匹配时,比较T[1]P[0],根据式(1),我们知道

T[0] == P[0],所以我们只需要知道P[0]是否等于P[1]就可以确定T[1]是否等于P[0]

这里我们已经知道P[0] == ‘a’,P[1] == ‘b’,P[0] 是不等于P[1],所以T[1]不用和P[0]比较

好了,继续看第三次朴素匹配需不需要做:

第三次匹配

序号

0

1

2

3

4

5

6

7

8

目标串

a

b

a

a

b

a

b

B

a

模式串

 

 

a

b

a

b

 

 

 

从第一次匹配结果,即式(1),我们知道T[2] == P[3]

这里只需要比较P[0]是否等于P[2],从这里看出是相等的,所以第三次匹配是必要的,这次的匹配从T[3]P[1]开始,注意到目标串的序号并不会回退

 

所以KMP的动机是:不要让目标指针回退,减少重复比较。

3.       一般性推导

前面有了感性认识后,我们来进行一般性推导

 (1)KMP要解决问题

在第s+1趟匹配中,假如匹配过程在模式串的前j个都匹配成功,

T[s,(s+1),….(s+j)] == P[0,1,…j] --- (2)

而在P[j+1]时失配,那么该回到模式串的第几个位置开始匹配呢?

 

 

(2)推导

         假设在第j+1时匹配失败,那么有:

 

深入理解KMP算法_第1张图片

 

 

 

TP的关系在上图中清楚看出来了,根据前面的朴素算法的思想,我们会进行这样的试探:

s+2次匹配(试探)

序号

s

s+1

s+2

s+3

…….

s+j-1

s+j

s+j+1

目标串

T[s]

T[s+1]

T[s+2]

T[s+3]

….

T[s+j-1]

T[s+j]

T[s+j+1]

 

模式串移动前

P[0]

P[1]

P[2]

P[3]

….

P[j-1]

P[j]

P[j+1]

 

模式串

移动后

 

P[0]

P[1]

P[2]

….

P[j-2]

P[j-1]

 

 

如果有P[0,1,2…j-1] != P[1,2,…j],那么我们知道这个必然是失配的

那么再移一位又怎样?这样我们自然而然去做右移试探:

s+3次匹配(试探)

序号

s

s+1

s+2

s+3

…….

s+j-1

s+j

s+j+1

目标串

T[s]

T[s+1]

T[s+2]

T[s+3]

….

T[s+j-1]

T[s+j]

T[s+j+1]

 

模式串移动前

P[0]

P[1]

P[2]

P[3]

….

P[j-1]

P[j]

P[j+1]

 

模式串

移动后

 

 

P[0]

P[1]

….

P[j-3]

P[j-2]

 

 

这里比较P[0,1,…j-2]P[2,3…j]

 

整个流程:

 

 

 

到这里或许你已经看出来了,我们只需要找到一个位置k,使:

P[0,1,2…k] == P[j-k,j-k+1,…..j]

成立。如下图:

 

 深入理解KMP算法_第2张图片

本质:找到一个最大的位置K,使在前一次匹配成功的子模式串的前缀等于它的后缀

next[j] = k (0<= k < j ),找不到k就表示为-1

 

4.       手工计算

这里举一个例子,来讲next的计算

 

模式串“abababc

子串长度

子串

Next

说明

1

a

-1

找不到k

2

ab

-1

找不到k

3

aba

0

P[0] = P[2] k==0

4

abab

1

P[0,1] = P[1,2]

5

ababa

2

P[0,1,2] = P[2,3,4]

6

ababab

3

P[0,1,2,3] = P[2,3,4,5]

7

abababc

-1

找不到k

 

5.       程序实现计算next

直接套公式计算是可行,但复杂度较高

用递推的方法做则比较好.

next(j) = k,则有

P[0,1,2…k] = P[j-k,j-k+1,…j]   ---  (3)

我们来计算next(j+1) = k’

先套定义:P[0,1,2….k’] = P[j-k’,j-k’+1,….j+1]   ---(4)

情况1

(3)式看出,如果P[k+1] == P[j+1],则必有k’ = k+1,next(j+1) = next(j)+1;

情况2

如果P[k+1] != P[j+1],那么只能后退找最长前缀了,可以用反证法证明不能前进找。

从式(2)看出,必须找出一个最大h,使:

P[0,1,2…h ] = P[k-h,k-h+1,…k] (h = next(k))

         这时如果P[h+1] == P[j+1],那么h就是所求,否则按上面步骤一直求直到h = -1(找不到)

         下面给出C++实现代码:

 

void ComputeNext(char *_str,const int _length,int* _next)

{

       _next[0] = -1;

       int k = -1;

 

       //递推求next[j]

       for(int j = 1; j < _length; ++j)

       {

              //P[k+1]P[j]不等时,一直后退,这里的j、也即文中求的j+1

              while(k >= 0 && _str[k+1] != _str[j])

              {

                     k = _next[k];

              }

              //相等时next = k + 1

              if(_str[k+1] == _str[j])

                     ++k;

              _next[j] = k;

       }

}

 

7.程序实现KMP

//Kmp算法

int FindKmp(char *chSrc,char *pTemplate,int sizeTemplate,int *next)

{

       int pointerToSrc = 0;

       int ponterToTemp = 0;

       size_t lengthSrc = strlen(chSrc);

       while( (ponterToTemp < (int)sizeTemplate) && (pointerToSrc < (int)lengthSrc) )

       {

              //匹配,两指针前进

              if(chSrc[pointerToSrc] == pTemplate[ponterToTemp])

              {

                     ++pointerToSrc;

                     ++ponterToTemp;

              }

              else

              {

                     //不等于-1时模式指针回退

                     if(next[ponterToTemp] != -1)

                            ponterToTemp = next[ponterToTemp];

                     else

                     {

                            //前面已比较的子串已经没有匹配的可能,源指针前进,模式指针归

                            ponterToTemp = 0;

                            ++pointerToSrc;

                     }

              }

       }

       if(ponterToTemp >= (int)sizeTemplate)

              return pointerToSrc - ponterToTemp +1;

       return -1;

}

 

一个KMP的寻找子串的函数如下:

//interface

int FindChildChar(char *chSrc,char *chCmp)

{

       int nLenCmp = strlen(chCmp);

       int *next = new int [nLenCmp];

       if(nLenCmp <= 0)

              return -1;

       computeNext(chCmp,nLenCmp,next);

       int ret  = FindKmp(chSrc,chCmp,nLenCmp,next);

       delete []next;

       return ret;

}

 

参考:

         《算法导论》

         《数据结构-基于C++面向对象》

你可能感兴趣的:(数据结构,c,工作,算法,delete)