KMP算法笔记

子串的定位操作通常称做串的模式匹配,是各种串处理系统中最重要的操作之一.在很多应用中都会涉及子串的定位问题,如普通的字符串查找问题.如果我们把模式匹配的串看成一字节流的话,那应用空间一下子就广阔了很多,HTTP协议里就是字节流,有各种关键的字节流字段,HTTP数据进行解释就需要用到模式匹配算法.

模式匹配算法里两个最为重要的算法:KMPBM算法

图2-1 KMP算法的一个例子

                        

    如果是普通的匹配算法,那么接下来,模式串的下一个匹配将如上一节读者所看到的那样,回溯到第二个位置b处。而KMP算法会怎么做呢?KMP算法会直接把模式串移到匹配失效的位置上,如下图2-2,g处

                       图2-2 直接移到匹配失效的位置g处

 Ok,咱们下面再看一个例子,如下图2-3/4:

                       图2- 3/4 另一个例子

如果前面有匹配成功,那移动一位或者几位后,是不可能匹配成功的

KMP算法快在于,i只加从不减,只有短串在长串中快速滑动。


代码:

  1. int kmp_search(char const* src, int slen, char const* patn, int plen, int const* nextval, int pos)  
  2. {  
  3.     int i = pos;  
  4.     int j = 0;  
  5.     while ( i < slen && j < plen )  
  6.     {  
  7.         if( j == -1 || src[i] == patn[j] )  
  8.         {  
  9.             ++i;  
  10.             ++j;   //匹配成功,就++,继续比较。  
  11.         }  
  12.         else  
  13.         {  
  14.             j = nextval[j];            
  15.             //当在j处,P[j]与S[i]匹配失败的时候直接用patn[nextval[j]]继续与S[i]比较,  
  16.             //所以,Kmp算法的关键之处就在于怎么求这个值拉,  
  17.             //即匹配失效后下一次匹配的位置。下面,具体阐述。  
  18.         }  
  19.     }  
  20.     if( j >= plen )  
  21.         return i-plen;  
  22.     else  
  23.         return -1;  
  24. }   

我们用变量k来代表求得的j_next的最大值,即k表示这S[i]、P[j]不匹配时P中下一个用来匹配的位置,使得P[0…k-1] = P[j-k…j-1],而我们要尽量找到这个k的最大值。如你所见,当匹配到S[i] != P[j]的时候,最大的k为1(当S[i]与P[j]不匹配时,用P[k]与S[i]匹配,即P[1]和S[i]匹配,因为P[0]=P[2],所以最大的k=1)。

                图3-2 j_next=1,即最大的k的值为1

  如上图3-2,当P[3]!=S[i],而P[0]=P[2](当P[3]!=S[i],而P[0]=P[2],P[2]=S[i-1],所以肯定有P[0]=S[i-1])),所以只需比较P[1]与S[i]就可以了,即k是P可以跳过比较的最大长度,换句话说,就是k能标示出S[i]与P[j]不匹配时P的下一个匹配的位置。

图3-3 第二步匹配中,跳过P[0](a),只需要比较 P[1]与S[4](b)了

也就是说,如上图3-3,在第一次匹配中,就是因为S[3]=P[0],所以在下一次匹配中,只需要比较S[4]=P[1],跳过了几步?一步。那么k等于多少?k=1。即把 P 右移两个位置后,P[0]与S[3]不必再比较,因为前一步已经得出他们相等。所以,此时,只需要比较 P[1]与S[4]了。

接下来的问题是,怎么求最大的数k使得p[0…k-1] = p[j-k…j-1]呢。这就是KMP算法中最核心的问题,即怎么求next数组的各元素的值?只有真正弄懂了这个next数组的求法,你才能彻底明白KMP算法到底是怎么一回事。
那么,怎么求这个next数组呢?咱们一步一步来考虑。
求最大的数k使得P[0…k-1] = P[j-k…j-1],一个直接的办法是对于j,从P[j-1]往回查,看是否有满足P[0…k-1] = P[j-k…j-1]的k存在,而且还要最大的一个k。下面咱们换一个角度思考。
当P[j+1]与S[i+1]不匹配时,分两种情况求next数组(注:以下皆有k=next[j]):

  1. P[j] = p[k], 那么next[j+1]=k+1,这个很容易理解。采用递推的方式求出next[j+1]=k+1(代码3-1的if部分)。
  2. P[j] != p[k],那么next[j+1]=next[k]+1(代码3-1的else部分)

稍后,你将看到,由这个方法得出的next值还不是最优的,也就是说是不能允许P[j]=P[next[j]]出现的。ok,请跟着我们一步一步登上山顶,不要试图一步登天,那是不可能的。由以上,可得如下代码:

  1. //代码3-1,稍后,你将由下文看到,此求next数组元素值的方法有错误  
  2. void get_next(char const* ptrn, int plen, int* nextval)  
  3. {  
  4.     int i = 0;   
  5.     nextval[i] = -1;  
  6.     int j = -1;  
  7.     while( i < plen-1 )  
  8.     {  
  9.         if( j == -1 || ptrn[i] == ptrn[j] )    //循环的if部分  
  10.         {  
  11.             ++i;  
  12.             ++j;  
  13.             nextval[i] = j;  
  14.         }  
  15.         else                         //循环的else部分  
  16.             j = nextval[j];             //递推  
  17.     }  
  18. }  

但程序仍不算最优,举个例子:

                     图3-13/14 求next数组各值的错误解法

按照之前的算法,S串和T串在i = 3 和 j = 3 处时不匹配,j = next[i] = 1,即取出T[1] = b的值跟S[3]匹配,但其实T[1] == T[3] == b,相当于是废步。因此若要优化,要从此处着手。
即排除T[j] == T[next[j]]的情况,故,在if里边再加个判断
if(T[i] != T[j])
   next[i] = j;
else
next[i] = next[j];    //前缀==后缀的情况要区别单ling出来

按这个逻辑,再走一遍上边那个例子:

初始化:nextval[0] = -1,我们得到第一个next值即-1.

            图4-2 第一个next值即-1

    i = 0,j = -1,由于j == -1,进入上述循环的if部分,++i得i=1,++j得j=0,且ptrn[i] != ptrn[j](即a!=b)),所以得到第二个next值即nextval[1] = 0;

            图4-3 第二个next值0

   上面我们已经得到,i= 1,j = 0,由于不满足条件j == -1 || ptrn[i] == ptrn[j],所以进入循环的esle部分,得j = nextval[j] = -1;此时,仍满足循环条件,由于i = 1,j = -1,因为j == -1,再次进入循环的if部分,++i得i=2,++j得j=0,由于ptrn[i] == ptrn[j](即ptrn[2]=ptrn[0],也就是说第1个元素和第三个元素都是a),所以进入循环if部分内嵌的else部分,得到nextval[2] = nextval[0] = -1

         图4-4 第三个next数组元素值-1

    i = 2,j = 0,由于ptrn[i] == ptrn[j],进入if部分,++i得i=3,++j得j=1,所以ptrn[i] == ptrn[j](ptrn[3]==ptrn[1],也就是说第2个元素和第4个元素都是b),所以进入循环if部分内嵌的else部分,得到nextval[3] = nextval[1] = 0;

最终,代码确定为:

  1. //代码4-1  
  2. //修正后的求next数组各值的函数代码  
  3. void get_nextval(char const* ptrn, int plen, int* nextval)  
  4. {  
  5.     int i = 0;   
  6.     nextval[i] = -1;  
  7.     int j = -1;  
  8.     while( i < plen-1 )  
  9.     {  
  10.         if( j == -1 || ptrn[i] == ptrn[j] )   //循环的if部分  
  11.         {  
  12.             ++i;  
  13.             ++j;  
  14.             //修正的地方就发生下面这4行  
  15.             if( ptrn[i] != ptrn[j] ) //++i,++j之后,再次判断ptrn[i]与ptrn[j]的关系  
  16.                 nextval[i] = j;      //之前的错误解法就在于整个判断只有这一句。  
  17.             else  
  18.                 nextval[i] = nextval[j];  
  19.         }  
  20.         else                                 //循环的else部分  
  21.             j = nextval[j];  
  22.     }  
  23. }  


我想KMP学习可以暂时告一段落,随着体会的深入看能不能有新的认识。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值