KMP算法总结

本文深入讲解了KMP算法的工作原理及实现过程,对比了BF算法的不足之处,并详细阐述了KMP算法中next数组和nextval数组的计算方法。
要想理解KMP,还要先从BF(Brute-Force,最基本的字符串匹配算法的)算法谈起。要先了解它是怎么个流程,然后有什么缺点,就能进一步了解KMP要解决的什么问题。

1.BF算法

现在假设S串是主串,T串是模式串,i,j分别是S和T中正待比较的位置,pos是指S串开始比较的位置。
算法的思想是:从主串S的pos位置处开始与模式串的第一个字符开始进行比较,如果相等,继续比较主串S的下一位和模式串的下一位,如果不相等,则从主串的下一个字符起与模式串的第一个字符再开始比较。依次类推,直到模式串中的每一个字符都与主串S中的某一段连续的字符都完全匹配为止,函数返回主串中跟模式串第一个完全匹配的一段连续字符的开始位置,否则就是匹配不成功,函数返回零。
上代码:

  1. int Index(SString S, SString T, int pos) {  
  2.    //返回T在S中第pos个字符之后的位置  
  3.    i=pos; j=1; 
  4.   while ( i< = S[0] && j< = T[0] ) {  //S[0],T[0],存储的是S和T的字符串的长度
  5.       if (S[i] = = T[j] ) {++i;  ++j;}   //继续比较后续字符  
  6.       else {i=i-j+2;   j=1;}      //指针回溯到 下一首位,重新开始  (i和j同时递增,所以i-j+2就相当于pos-1+2=pos+1,也就是下一个比较的位置是在s串中向后移动了一位)
  7.   }  
  8.   if(j>T[0]) return i-T[0];          //子串结束,说明匹配成功  
  9.   else return  0;  
  10. }//Index 
下面来看下缺点:这个算法,如果进行比较的过程中,跟模式串中有一个不匹配,主串的i都要回溯,再跟模式串去进行比较。在最坏的情况下,每一次比对到模式串的最后一位时不匹配,主串的i回溯到最开始比对的位置的下一位,模式串中的 j 变为0,时间复杂度相当于O( strlen(s)*strlen(t) )  。
比如在模式串为aaaaaab,主串为aaaaaaaaaaaaaaaaaaaaaaaaaaaab的这种情形中
            i=7
aaaaaaaaaaaaaaaaaaaaaaaaaaaab
aaaaaab
            j=7
比对到模式串中的 b 的位置不匹配,然后主串的 i 的位置移动到下一位,模式串 j 变为1。如下所示
  i=2
aaaaaaaaaaaaaaaaaaaaaaaaaaaab
  aaaaaab
  j=1
当再一次比对到模式串中的 b 的位置不匹配,然后主串的 i 的位置移动到下一位,模式串 j 变为1。如下所示
    i=3
aaaaaaaaaaaaaaaaaaaaaaaaaaaab
   aaaaaab
   j=1

这样每一轮 i 都要回溯,每比较7次,i都要回溯一次,i总共要回溯7*22=154次。

再看下一种情况,模式串abcdbbabcdbb,主串abcdbbabcdbdabcdbbabcdbb,从第一个位置开始比较 i=1,j=1.
                     i=12
abcdbbabcdbdabcdbbabcdbb
abcdbbabcdbb
                     j=12

当比较到标红的位置d时不匹配,此时i = 12,j=12 。
如果按照BF算法,就必须得如下回溯比较才能开始下一轮的匹配 

  i=2
abcdbbabcdbdabcdbbabcdbb
  abcdbbabcdbb
  j=1

而经过观察,完全可以 i 不用回溯

                      i=12
abcdbbabcdbdabcdbbabcdbb
            abcdbbabcdbb
                      j=6

因为在i=2,j=1   i=3,j=1   i=4,j=1   i=5,j=1   i=6,j=1这几次比较都是不必进行的。
很显然的是第一轮比较的结果得知,主串中的第7、8、9、10、11必然是abcdb,匹配串的前五个也是abcdb,所以直接i = 12,j= 6进行去比较,明显加快字符串匹配的步伐。

所以说,引出来要解决的问题就是,怎样在 i 不回溯的情况下,当主串中某个字符和模式串的第 j 个字符不匹配的时候,寻找下一轮模式串中与主串中该字符进行匹配位置,即寻找 j 的新值,将模式串向右滑行更远的距离与主串进行匹配,减少比对的次数,加快匹配的速度。
KMP算法就是来解决这个问题的。

2 KMP算法

先看下KMP源代码:

  1. int Index_KMP(SString S, SString T, int pos) {  
  2.    //返回T在S中第pos个字符之后的位置  
  3.    i=pos; j=1; 
  4.   while ( i< = S[0] && j< = T[0] ) {  //S[0],T[0],存储的是S和T的字符串的长度
  5.       if (S[i] = = T[j] || j==0) {++i;  ++j;}   //继续比较后续字符  
  6.       else {
  7.              j = next[j]; //模式串向右移动
  8.            }      
  9.   }  
  10.   if(j>T[0]) return i-T[0];          //子串结束,说明匹配成功  
  11.   else return  0;  
  12. }//Index 

跟BF算法比较一下,会发现,整个函数没什么区别,就是在不匹配的时候,只有 j 的位置发生变化,j不是变为1,而是为next[j]

所以说,KMP的核心在于求next[j],next[j]的含义是:当模式串中第 j 个位置和主串中不匹配时,模式串下一轮和主串中该字符进行比较的位置。

以下可以推导能让你深入理解next[j]的含义:

假如在第j个位置不匹配,应该用模式串中的第k个字符去和主串去匹配,即next[j]=k(1<k<j),那说明不存在k'>k,使下列条件成立

T1 T2 T3 T4... TK-1 = Si-K+1...Si-4 Si-3 Si-2 Si-1  (1)

而又因为模式串的前j-1个都匹配成功了,1<k<j, 所以肯定有

Tj-k+1 Tj-k+2 T j-k+3Tj-k+4... Tj-1 = Si-K+1...Si-4 Si-3 Si-2 Si-1   (2)

必定有模式串中的前j-1个字符串中的后k-1个和主串中前i-1中字符中的后k-1个字符串相匹配
 由上面的(1),(2)可得 T1 T2 T3 T4... TK-1 = Tj-k+1 Tj-k+2 T j-k+3Tj-k+4... Tj-1
这个结论的意思就是:如果第i位和模式串中的第j位不匹配时,应该用模式串中的第k个字符去和主串去匹配,即next[j]=k, 那么必定在前j-1个模式串的子串中,存在一个长度为k的相匹配的前缀和后缀。

看个例子你就明白了,拿这个例子来举例:

第12位不匹配
                         i=12
S abcdbbabcdbdabcdbbabcdbb
T abcdbbabcdbb
                          j=12

很容易观察得到 next[12]=6 ,就是模式串中第12个跟主串的字符不匹配时,下一次用模式串的第6个字符去跟主串的该字符去比较
                          i=12
S abcdbbabcdbdabcdbbabcdbb
T             abcdbbabcdbb
                         j=6

观察得
next[12]=6,所以由上面的推导可知:
TTTT4... TK-1 = TTTT4 T5 = abcdb
Si-K+1...Si-4 Si-3 Si-2 Si-1= S7SSS10 S11=abcdb

即如下所示

S abcdbbabcdbdabcdbbabcdbb
T             abcdbbabcdbb

因为前11个匹配成功,有TTTT4 TTTTT9 T10 T11= SSSSS5 S6 SSSS10 S11
肯定有TTT9 T10 T11 = SSSS10 S11 ,所以TTTT4 T5 = TTT9 T10 T11 abcdbbabcdbb中,前11个中,前五个前缀是跟后五个后缀是完全匹配的,即abcdbbabcdbb,红色的完全匹配

由上面的例子得出:如果主串中第i位和模式串中的第j位不匹配时,next[j]的值就相当于在模式串的前j-1位字符子串中,寻找使 TTTT4... TK-1 = Tj-k+1 Tj-k+2 T j-k+3Tj-k+4... Tj-1 成立的最大的k值,也就是在前j-1个字符子串中,使前缀和后缀相匹配的最大字符串的长度。

这个地方理解了,你后面就差不多能理解了!


得出结论next[j]的定义:

                 0   如果j=1(规定,第一个都不匹配,那主串就直接跳到下一个字符去和模式串的第一个去匹配了)
next[j]= Max{k|1<k<j且'p1...pk-1'='pj-k+1...pj-1'}当此集合不空时
                 1   其它情况

再回头看KMP的源代码,它的匹配过程可以描述为:如果在匹配过程中Si和Tj相等,则i和j分别增加1,否则i不变,j退到next[j],然后继续比较,若相等,各增加1,否则j再退到next[j],依此类推,直到遇到下面两种情形:
(1)j退到next[j],之后比较,相等,i,j各自增1,继续比对Si和Tj。
(2)j变为0(也就是和模式串的第一位都不匹配),i,j各自增1,继续比对
Si和Tj。

那么如何求next[j]呢?
由定义知道 next[1] = 0
假设next[j] = k,则由定义知,必有TTTT4... TK-1 = Tj-k+1 Tj-k+2 T j-k+3Tj-k+4... Tj-1 ,其中k<j,不存在k'>k 使上述条件成立.那如何推导next[j+1]?
假如Tk = Tj的话,就说明TTTT4... TK-1 TkTj-k+1 Tj-k+2 T j-k+3Tj-k+4... Tj-1 Tj,又因为next[j]=k,所以上面的推导的定义可知next[j+1] = k+1;
假如Tk 不等于 Tj,那么就可以这样,可以把TTTT4... TK-1 Tk当成模式串,整个原来的模式串当成主串,如下图1所示,就相当于前k-1个匹配成功,当第k个不匹配时,寻找next[k], 假设next[k]=n,
TTTT4... Tn-1 = Tk-n+1 Tk-n+2 Tk-n+3 ... TK-2 Tk-1 ,
假如Tn = Tj的话,如图2所示,那就说明肯定存在
TTTT4... Tn-1 Tn= Tj-n+1 Tj-n+2 T j-n+3Tj-n+4... Tj-1 Tj
所以next[k+1] =n+1 = next[k]+1
                                                                      图1
                                                                          图2

如果Tn 和 Tj不相等的话,然后就再把TTTT4... Tn-1 Tn当成模式串,整个原来的模式串当成主串,又相当于第n个和主串的第j个没有匹配成功,寻找下一个next[n]的位置,继续递归下去,直到Tj和某个字符(假设位置为k)匹配成功,next[j+1] = k+1,或者没有字符跟Tj匹配成功,next[j+1] = 1。

next[]的手工求解方法:
1.前两位的next的值必为0,1,第一位为0,这是规定的,第二位为1,是因为定义中你找不到符合的k值满足
Max{k|1<k<j且'p1...pk-1'='pj-k+1...pj-1'}当此集合不空时  

                0   如果j=1(规定)
next[j]= Max{k|1<k<j且'p1...pk-1'='pj-k+1...pj-1'}当此集合不空时  
                 1   其它情况

所以说是其他情况,所以说next[2]=1 
2.往后的位置看前一位的next值,如果Tj-1= Tnext[j-1],那么next[j]= next[j-1]+1
如果Tj-1与Tnext[j-1]不相等,比较Tj-1和Tnext[next[j-1]], 依次类推,遇到两种情况终止,要么,比较到Tj-1与T1还不相等,那么next[j]=1,要么,比较到Tj-1与Tk相等,那么next[j] = k+1;

举例说明:
        前两个位置为0,1

1 2 3 4 5 6 7 8
a b a a b c a c
0 1

第三个位置,j=3,Tj-1 = T2 ,Tnext[j-1]= Tnext[2]=T1,T2与T1不相等,因为比较到了T1还不相等,那么next[3] = 1
1 2 3 4 5 6 7 8

a b a a b c a c
0 1 1

第四个位置,j=4,Tj-1 = T3 ,Tnext[j-1]= Tnext[3]=T1,T3 = T1,所以next[4] = next[3]+1=2

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2

第五个位置,j=5,Tj-1 = T4 ,Tnext[j-1]= Tnext[4]=T2,T4 与 T2不相等,然后求 Tnext[next[j-1]]=Tnext[2]=T1 , T4和T1相等,所以next[5] =1+1=2

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2

第六个位置,j=6,Tj-1 = T5 ,Tnext[j-1]= Tnext[5]=T2,T5 = T2,next[6] =next[5]+1=3

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2 3

第七个位置,j=7,Tj-1 = T6 ,Tnext[j-1]= Tnext[6]=T3,T6 与 T3 不相等,然后求Tnext[next[j-1]] = Tnext[3]=T1,T6和T1也不相等,所以由刚才的定义知,next[7] = 1

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2 3 1

第八个位置,j=8,Tj-1 = T7 ,Tnext[j-1]= Tnext[7]=T1,T7 = T1 相等,next[8] = next[7]+1=2

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2 3 1 2
求解完毕

那么nextval是怎么回事呢?

下面来看这一种情况:
   1 2 3 4 5 6 7 8 9 10 11 12 13
S a b a a b c d e f  k   a   b   a
T a b a a b a
   0 1 1 2 2 3
下面红色的是next值
当S6和T6不匹配时,按照next值,我们要这样调整
   1 2 3 4 5 6 7 8 9 10 11 12 13
S a b a a b c d e f  k   a   b   a
T          a b a b a
            0 1 1 2 2 3
发现S6和T3也不匹配,按照next值,我们要这样调整
   1 2 3 4 5 6 7 8 9 10 11 12 13
S a b a a b c d e f  k   a   b   a
T               a b a a b a
                 0 1 1 2 2 3
发现S6和T1也不匹配,按照next值,我们要这样调整
   1 2 3 4 5 6 7 8 9 10 11 12 13
S a b a a b c d e f  k   a   b   a
T                  a b a a   b   a
                    0 1 1 2   2   3

有没有发现,我们为何不把next[6]=0,这样,就不用比较S6和T3,S6和T1了,这样一步到位,就直接比较S7和T1

所以说要对next进行改进,去除不必要的比较,这样就是nextval[]的含义

其实你应该会发现,如果next[j] = k, 说明在模式串与主串匹配中,模式串前j-1个匹配成功,第j个匹配不成功,即Si 不等于Tj ,那么如果Tj = Tk的话,就说明在模式串前k位与模式串,又相当于一个子的模式匹配问题,现在的模式串是模式串前k位,相当于模式串前k位与主串匹配时,前k-1个匹配成功,第k个匹配不成功,即Si也不等于Tk。所以,这一次比较是多余的,完全可以略过,继续往前找,看Tj是否和Tnext[k]匹配,以此类推,一直找到和Tj不匹配的字符为止。
如上面的例子一样,next[6]= 3 ,T6=T3,就可以略过去,下一次比对的时候,next[3]=1,T3 = T1,又可以略过去,依次类推。
如果Tj不和Tk匹配,就说明,有可能Si和Tk会相等,就不能略过去.


nextval[]的手工求解方法:
先按照上面说的next[]的手工求解方法求出next[],因为有了next[]的值之后,我们就能知道Tj和Tnext[j]是否匹配,手工求解更方便。
1,第1位nextval值为0。
2,第2位起,先看next[j]的值,next[j] = k
1)如果Tj不等于Tnext[j],那么nextval[j]=next[j] = k;
2)Tj=Tnext[j]的话,继续比较Tj和Tnext[next[j]],依次类推,继续和下一位next值的位置上的字符相比较,直到比较到Tj和Tnext[k]不等为止,nextval[j] =next[k],或者比较到Tj=T1,那么nextval[j]=0。

以上面的例子为例,
前两个位置为0,1
1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2 3 1 2
0 1 

第三个位置,j=3,Tj = T3 ,next[j] = next[3] =1,Tnext[j]= Tnext[3]=T1,T3与T1相等,因为比较到了T1,相等,那么next[3] = 0

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2 3 1 2
0 1 0

第四个位置,j=4,Tj = T4 ,Tnext[j]= Tnext[4]=T2,T4 与 T2不相等,所以nextval[4] = next[4]=2

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2 3 1 2
0 1 0 2

第五个位置,j=5,Tj = T5 ,Tnext[j]= Tnext[5]=T2,T5= T2,然后求 Tnext[next[j]]=Tnext[2]=T1 , T5和T1不相等,所以nextval[5] =next[2]=1

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2 3 1 2
0 1 0 2 1

第六个位置,j=6,Tj = T6 ,Tnext[j]= Tnext[6]=T3,T6 与 T3不相等,nextval[6] =next[6]=3

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2 3 1 2
0 1 0 2 1 3 

第七个位置,j=7,Tj= T7 ,Tnext[j]= Tnext[7]=T1,T7 与 T1 相等,因为比较到了T1,相等,那么nextval[7] = 0

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2 3 1 2
0 1 0 2 1 3 0

第八个位置,j=8,Tj = T8 ,Tnext[j]= Tnext[8]=T2,T8 与T2 不相等,nextval[8] = next[8]=2

1 2 3 4 5 6 7 8
a b a a b c a c
0 1 1 2 2 3 1 2
0 1 0 2 1 3 0 2

求解完毕




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值