kmp算法看的3篇博客(未整理)

来源:互联网 发布:ipad 抓包淘宝旧版本 编辑:程序博客网 时间:2024/06/09 21:01

讲manacher算法  http://blog.csdn.net/ggggiqnypgjg/article/details/6645824

On)回文子串算法

注:转载的这篇文章,我发现下面那个源代码有点bug。。。在下一篇博客中改正了。。

 

    这里,我介绍一下On)回文串处理的一种方法。Manacher算法.
原文地址:
http://zhuhongcheng.wordpress.com/2009/08/02/a-simple-linear-time-algorithm-for-finding-longest-palindrome-sub-string/
    其实原文说得是比较清楚的,只是英文的,我这里写一份中文的吧。
    首先:大家都知道什么叫回文串吧,这个算法要解决的就是一个字符串中最长的回文子串有多长。这个算法可以在On)的时间复杂度内既线性时间复杂度的情况下,求出以每个字符为中心的最长回文有多长,
    这个算法有一个很巧妙的地方,它把奇数的回文串和偶数的回文串统一起来考虑了。这一点一直是在做回文串问题中时比较烦的地方。这个算法还有一个很好的地方就是充分利用了字符匹配的特殊性,避免了大量不必要的重复匹配。
    算法大致过程是这样。先在每两个相邻字符中间插入一个分隔符,当然这个分隔符要在原串中没有出现过。一般可以用‘#’分隔。这样就非常巧妙的将奇数长度回文串与偶数长度回文串统一起来考虑了(见下面的一个例子,回文串长度全为奇数了),然后用一个辅助数组P记录以每个字符为中心的最长回文串的信息。Pid]记录的是以字符strid]为中心的最长回文串,当以strid]为第一个字符,这个最长回文串向右延伸了Pid]个字符。
    原串:    w aa bwsw f d
    新串:   # w# a # a # b# w # s # w # f # d #
辅助数组P:  1 2 1 2 3 2 1 2 1 2 1 4 1 2 1 2 1 2 1
    这里有一个很好的性质,Pid-1就是该回文子串在原串中的长度(包括‘#’)。如果这里不是特别清楚,可以自己拿出纸来画一画,自己体会体会。当然这里可能每个人写法不尽相同,不过我想大致思路应该是一样的吧。
    好,我们继续。现在的关键问题就在于怎么在On)时间复杂度内求出P数组了。只要把这个P数组求出来,最长回文子串就可以直接扫一遍得出来了。
    由于这个算法是线性从前往后扫的。那么当我们准备求Pi]的时候,i以前的Pj]我们是已经得到了的。我们用mx记在i之前的回文串中,延伸至最右端的位置。同时用id这个变量记下取得这个最优mx时的id值。(注:为了防止字符比较的时候越界,我在这个加了‘#’的字符串之前还加了另一个特殊字符‘$’,故我的新串下标是从1开始的)
好,到这里,我们可以先贴一份代码了。

复制代码

  1. void pk()
    {
        int i;
        int mx = 0;
        int id;
        for(i=1; i<n; i++)
        {
            if( mx > i )
                p[i] = MIN( p[2*id-i], mx-i );        
            else
                p[i] = 1;
            for(; str[i+p[i]] == str[i-p[i]]; p[i]++)
                ;
            if( p[i] + i > mx )
            {
                mx = p[i] + i;
                id = i;
            }
        }
    }

   代码是不是很短啊,而且相当好写。很方便吧,还记得我上面说的这个算法避免了很多不必要的重复匹配吧。这是什么意思呢,其实这就是一句代码。

if( mx > i)
    p[i]=MIN( p[2*id-i], mx-i);

就是当前面比较的最远长度mx>i的时候,Pi]有一个最小值。这个算法的核心思想就在这里,为什么P数组满足这样一个性质呢?
   (下面的部分为图片形式)




    看完这个算法,你有可能会觉得这种算法在哪会用到呢?其实回文串后缀数组也可以做。只是复杂度是On log n)的,而且一般情况下也不会刻意去卡一个log n的算法。可正好hdu就有这么一题,你用后缀数组写怎么都得T(当然应该是我写得太烂了)。不信的话大家也可以去试试这题。
        http://acm.hdu.edu.cn/showproblem.php?pid=3068
    另外,顺便附一份AC代码。
        http://acm.hust.edu.cn:8080/judge/problem/viewSource.action?id=140283

        题目就是求一个串的最大回文子串的字符个数。。。


[cpp] view plaincopy
  1. #include<vector>  
  2. #include<iostream>  
  3. using namespace std;  
  4.   
  5. const int N=300010;  
  6. int n, p[N];  
  7. char s[N], str[N];  
  8.   
  9. #define _min(x, y) ((x)<(y)?(x):(y))  
  10.   
  11. void kp()  
  12. {  
  13.     int i;  
  14.     int mx = 0;  
  15.     int id;  
  16.     for(i=n; str[i]!=0; i++)  
  17.         str[i] = 0; //没有这一句有问题。。就过不了ural1297,比如数据:ababa aba  
  18.     for(i=1; i<n; i++)  
  19.     {  
  20.         if( mx > i )  
  21.             p[i] = _min( p[2*id-i], p[id]+id-i );  
  22.         else  
  23.             p[i] = 1;  
  24.         for(; str[i+p[i]] == str[i-p[i]]; p[i]++)  
  25.             ;  
  26.         if( p[i] + i > mx )  
  27.         {  
  28.             mx = p[i] + i;  
  29.             id = i;  
  30.         }  
  31.     }  
  32. }  
  33.   
  34. void init()  
  35. {  
  36.     int i, j, k;  
  37.     str[0] = '$';  
  38.     str[1] = '#';  
  39.     for(i=0; i<n; i++)  
  40.     {  
  41.         str[i*2+2] = s[i];  
  42.         str[i*2+3] = '#';  
  43.     }  
  44.     n = n*2+2;  
  45.     s[n] = 0;  
  46. }  
  47.   
  48. int main()  
  49. {  
  50.     int i, ans;  
  51.     while(scanf("%s", s)!=EOF)  
  52.     {  
  53.         n = strlen(s);  
  54.         init();  
  55.         kp();  
  56.         ans = 0;  
  57.         for(i=0; i<n; i++)  
  58.             if(p[i]>ans)  
  59.                 ans = p[i];  
  60.         printf("%d\n", ans-1);  
  61.     }  
  62.     return 0;  
  63. }  
 

KMP算法详解(转)

 41955人阅读 评论(15) 收藏 举报
算法functiondelete数据结构search测试

目录(?)[+]

目录(?)[+]

作者:July。
出处http://blog.csdn.net/v_JULY_v/。

引记

    此前一天,一位MS的朋友邀我一起去与他讨论快速排序,红黑树,字典树,B树、后缀树,包括KMP算法,唯独在讲解KMP算法的时候,言语磕磕碰碰,我想,原因有二:1、博客内的东西不常回顾,忘了不少;2、便是我对KMP算法的理解还不够彻底,自不用说讲解自如,运用自如了。所以,特再写本篇文章。由于此前,个人已经写过关于KMP算法的两篇文章,所以,本文名为:KMP算法之总结篇。

   本文分为如下六个部分:

  1. 第一部分、再次回顾普通的BF算法与KMP算法各自的时间复杂度,并两相对照各自的匹配原理;
  2. 第二部分、通过我此前第二篇文章的引用,用图从头到尾详细阐述KMP算法中的next数组求法,并运用求得的next数组写出KMP算法的源码;
  3. 第三部分、KMP算法的两种实现,代码实现一是根据本人关于KMP算法的第二篇文章所写,代码实现二是根据本人的关于KMP算法的第一篇文章所写;
  4. 第四部分、测试,分别对第三部分的两种实现中next数组的求法进行测试,挖掘其区别之所在;
  5. 第五部分、KMP完整准确源码,给出KMP算法的准确的完整源码;
  6. 第六步份、一眼看出字符串的next数组各值,通过几个例子,让读者能根据字符串本身一眼判断出其next数组各值。

    力求让此文彻底让读者洞穿此KMP算法,所有原理,来龙去脉,让读者搞个通通透透注意本文中第二部分及第三部分的代码实现一的字符串下标i 从0开始计算,其它部分如第三部分的代码实现二,第五部分,和第六部分的字符串下标i 皆是从1开始的)。

    在看本文之前,你心中如若对前缀和后缀这个两个概念有自己的理解,便最好了。有些东西比如此KMP算法需要我们反复思考,反复求解才行。个人写的关于KMP算法的第二篇文章为:六(续)、从KMP算法一步一步谈到BM算法;第一篇为:六、教你初步了解KMP算法、updated(文末链接)。ok,若有任何问题,恳请不吝指正。多谢。

第一部分、KMP算法初解

1普通字符串匹配BF算法与KMP算法的时间复杂度比较

    KMP算法是一种线性时间复杂的字符串匹配算法,它是对BF算法(Brute-Force,最基本的字符串匹配算法的)改进。对于给的原始串S和模式串P,需要从字符串S中找到字符串P出现的位置的索引。

BF算法的时间复杂度O(strlen(S) * strlen(T)),空间复杂度O(1)

KMP算法的时间复杂度O(strlen(S) + strlen(T)),空间复杂度O(strlen(T))

2BF算法与KMP算法的区别

    假设现在S串匹配到i位置,T串匹配到j位置。那么总的来说,两种算法的主要区别在于失配的情况下,对[j] 的值做的处理

   BF算法中,如果当前字符匹配成功,即s[i+j] == T[j],令j++,继续匹配下一个字符;如果失配,即S[i + j] != T[j]需要让i++,并且j= 0,即每次匹配失败的情况下,模式串T相对于原始串S向右移动了一位。

    而KMP算法中,如果当前字符匹配成功,即S[i]==T[j],令i++j++,继续匹配下一个字符;如果匹配失败,即S[i] != T[j],需要保持i不变,并且让j = next[j],这里next[j] <=j -1,即模式串T相对于原始串S向右移动了至少1(移动的实际位数j - next[j]  >=1),

    同时移动之后,i之前的部分(即S[i-j+1 ~ i-1]),和j=next[j]之前的部分(即T[0 ~ j-2])仍然相等。显然,相对于BF算法来说,KMP移动更多的位数,起到了一个加速的作用! (失配的特殊情形,令j=next[j]导致j==0的时候,需要将i ++,否则此时没有移动模式串)

3、BF算法为什么要回溯

首先说一下为什么BF算法要回溯。如下两字符串匹配(恰如上面所述:BF算法中,如果当前字符匹配成功,即s[i+j] == T[j],令j++,继续匹配下一个字符):

      i+jjT中的j++变,而动)

S:aaaacefghij

         j++

T:aaac 

如果不回溯的话就是从下一位开始比起:

aaaacefghij

        aaac

看到上面红颜色的没,如果不回溯的话,那么从a 的下一位c 比起。然而下述这种情况就漏了(正确的做法当然是要回溯:如果失配,即S[i + j] != T[j]需要让i++,并且j= 0):

aaaacefghij

  aaac

    所以,BF算法要回溯,其代码如下:

view plain
  1. int Index(SString S, SString T, int pos) {  
  2.    //返回T在S中第pos个字符之后的位置  
  3.    i=pos; j=1;k=0;  
  4.   while ( i< = S[0] && j< = T[0] ) {  
  5.       if (S[i+k] = = T[j] ) {++k;  ++j;}   //继续比较后续字符  
  6.       else {i=i+1;   j=1; k=0;}      //指针回溯到 下一首位,重新开始  
  7.   }  
  8.   if(j>T[0]) return i;          //子串结束,说明匹配成功  
  9.   else return  0;  
  10. }//Index  

  不过,也有特殊情况可以不回溯,如下:
abcdefghij(主串)
abcdefg(模式串)
  即(模式串)没有相同的才不需要回溯。


4KMP 算法思想
    普通的字符串匹配算法必须要回溯。但回溯就影响了效率,回溯是由T串本身的性质决定的,是因为T串本身有前后'部分匹配'的性质。像上面所说如果主串为abcdef这样的,大没有回溯的必要。

    改进的地方也就是这里,我们从T串本身出发,事先就找准了T自身前后部分匹配的位置,那就可以改进算法。

    如果不用回溯,那模式串下一个位置从哪里开始呢?

    还是上面那个例子,T(模式串)ababc,如果c失配,那就可以往前移到aba最后一个a的位置,像这样:

...ababd...

   ababc

    ->ababc

这样i不用回溯,j跳到前2个位置,继续匹配的过程,这就是KMP算法所在。这个当T[j]失配后,j 应该往前跳的值就是jnext,它是由T串本身固有决定的,与S(主串)无关


5、next数组的含义

重点来了。下面解释一下next数组的含义,这个也是KMP算法中比较不好理解的一点。

  令原始串为: S[i],其中0<=i<=n;模式串为: T[j],其中0<=j<=m

  假设目前匹配到如下位置

               S0,S1,S2,...,Si-j,Si-j+1...............,Si-1, Si, Si+1,....,Sn

                                   T0,T1,.....................,Tj-1, Tj, ..........

  ST的绿色部分匹配成功,恰好到SiTj的时候失配,如果要保持i不变,同时达到让模式串T相对于原始串S右移的话,可以更新j的值,让Si和新的Tj进行匹配,假设新的jnext[j]表示,即让Sinext[j]匹配,显然新的j值要小于之前的j值,模式串才会是右移的效果,也就是说应该有next[j] <= j -1。那新的j值也就是next[j]应该是多少呢?我们观察如下的匹配:

      1)如果模式串右移1位(从简单的思考起,移动一位会怎么样),即next[j] = j - 1, 即让蓝色的SiTj-1匹配(注:省略号为未匹配部分)

               S0,S1,S2,...,Si-j,Si-j+1...............,Si-1, Si, Si+1,....,Sn

                                   T0,T1,.....................,Tj-1, Tj, .......... (T的划线部分和S划线部分相等【1】)

                                        T0,T1,.................Tj-2,Tj-1, ....... (移动后的T的划线部分和S的划线部分相等【2】)

        根据【1】【2】可以知道当next[j] =j -1,即模式串右移一位的时候,有T[0 ~ j-2] == T[1 ~ j-1],而这两部分恰好是字符串T[0 ~j-1]的前缀和后缀,也就是说next[j]的值取决于模式串Tj前面部分的前缀和后缀相等部分的长度(好好揣摩这两个关键字概念:前缀、后缀,或者再想想,我的上一篇文章,从Trie树谈到后缀树中,后缀树的概念)。

      2)如果模式串右移2位,即next[j] = j - 2, 即让蓝色的SiTj-2匹配    

               S0,S1,...,Si-j,Si-j+1,Si-j+2...............,Si-1, Si, Si+1,....,Sn

                                   T0,T1,T2,.....................,Tj-1, Tj, ..........(T的划线部分和S划线部分相等【3】)

                                              T0,T1,...............,Tj-3,Tj-2,.........(移动后的T的划线部分和S的划线部分相等【4】)

        同样根据【3】【4】可以知道当next[j] =j -2,即模式串右移两位的时候,有T[0 ~ j-3] == T[2 ~ j-1]。而这两部分也恰好是字符串T[0 ~j-1]的前缀和后缀,也就是说next[j]的值取决于模式串Tj前面部分的前缀和后缀相等部分的长度

     3)依次类推,可以得到如下结论:当发生失配的情况下,j的新值next[j]取决于模式串中T[0 ~ j-1]中前缀和后缀相等部分的长度, 并且next[j]恰好等于这个最大长度

    为此,请再允许我引用上文中的一段原文:KMP算法中,如果当前字符匹配成功,即S[i]==T[j],令i++j++,继续匹配下一个字符;如果匹配失败,即S[i] != T[j],需要保持i不变,并且让j = next[j],这里next[j] <=j -1,即模式串T相对于原始串S向右移动了至少1(移动的实际位数j - next[j]  >=1),

    同时移动之后,i之前的部分(即S[i-j+1 ~ i-1]),和j=next[j]之前的部分(即T[0 ~ j-2])仍然相等。显然,相对于BF算法来说,KMP移动更多的位数,起到了一个加速的作用 (失配的特殊情形,令j=next[j]导致j==0的时候,需要将i ++,否则此时没有移动模式串)。”

    于此,也就不难理解了我的关于KMP算法的第二篇文章之中:当匹配到S[i] != P[j]的时候有 S[i-j…i-1] = P[0…j-1]. 如果下面用j_next去匹配,则有P[0…j_next-1] = S[i-j_next…i-1] = P[j-j_next…j-1]。此过程如下图3-1所示。

  当匹配到S[i] != P[j]时,S[i-j…i-1] = P[0…j-1]

S: 0 … i-j … i-1 i …

P:       0 …   j-1 j …

  如果下面用j_next去匹配,则有P[0…j_next-1] = S[i-j_next…i-1] = P[j-j_next…j-1]。
所以在P中有如下匹配关系(获得这个匹配关系的意义是用来求next数组)

P: 0 … j-j_next  .…j-1_    …

P:        0    … .j_next-1 

  所以,根据上面两个步骤,推出下一匹配位置j_next:

S: 0 … i-j … i-j_next …   i-1      i …

P:                   0   … j_next-1 j_next …

             图3-1 求j-next(最大的值)的三个步骤

    下面,我们用变量k来代表求得的j_next的最大值,即k表示这S[i]、P[j]不匹配时P中下一个用来匹配的位置,使得P[0…k-1] = P[j-k…j-1],而我们要尽量找到这个k的最大值。”。

      根据上文的【1】与【2】的匹配情况,可得第二篇文章之中所谓的k=1(如aaaa的形式),根据上文的【3】与【4】的匹配情况,k=2(如abab的形式)。

     所以,归根究底,KMP算法的本质便是:针对待匹配的模式串的特点,判断它是否有重复的字符,从而找到它的前缀与后缀,进而求出相应的Next数组,最终根据Next数组而进行KMP匹配。接下来,进入本文的第二部分。

第二部分、next数组求法的来龙去脉与KMP算法的源码

    本部分引自个人此前的关于KMP算法的第二篇文章:六之续、由KMP算法谈到BM算法。前面,我们已经知道即不能让P[j]=P[next[j]]成立成立。不能再出现上面那样的情况啊!即不能有这种情况出现:P[3]=b,而竟也有P[next[3]]=P[1]=b

    正如在第二篇文章中,所提到的那样:“这里读者理解可能有困难的是因为文中,时而next,时而nextval,把他们的思维搞混乱了。其实next用于表达数组索引,而nextval专用于表达next数组索引下的具体各值,区别细微。至于文中说不允许P[j]=P[next[j] ]出现,是因为已经有P[3]=b与S[i]匹配败,而P[next[3]]=P1=b,若再拿P[1]=b去与S[i]匹配则必败。”--六之续、由KMP算法谈到BM算法。

   又恰恰如上文中所述:“模式串T相对于原始串S向右移动了至少1(移动的实际位数j - next[j]  >=1)

    ok,求next数组的get_nextval函数正确代码如下:

view plain
  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. }    

    举个例子,举例说明下上述求next数组的方法。
S a b a b a b c
P a b a b c
S[4] != P[4]
    那么下一个和S[4]匹配的位置是k=2(也即P[next[4]])。此处的k=2也再次佐证了上文第3节开头处关于为了找到下一个匹配的位置时k的求法。上面的主串与模式串开头4个字符都是“abab”,所以,匹配失效后下一个匹配的位置直接跳两步继续进行匹配。
S a b a b a b c
P      a b a b c
匹配成功

P的next数组值分别为-1 0 -1 0 2

    next数组各值怎么求出来的呢?分以下五步:

  1. 初始化:i=0,j=-1,nextval[0] = -1由于j == -1,进入上述循环的if部分,++i得i=1,++j得j=0,且ptrn[i] != ptrn[j](即a!=b)),所以得到第二个next值即nextval[1] = 0;
  2. i=1,j=0,进入循环esle部分,j=nextval[j]=nextval[0]=-1;
  3. 进入循环的if部分,++i,++j,i=2,j=0,因为ptrn[i]=ptrn[j]=a,所以nextval[2]=nextval[0]=-1;
  4. i=2, j=0, 由于ptrn[i]=ptrn[j],再次进入循环if部分,所以++i=3,++j=1,因为ptrn[i]=ptrn[j]=b,所以nextval[3]=nextval[1]=0;
  5. i=3,j=1,由于ptrn[i]=ptrn[j]=b,所以++i=4,++j=2,退出循环。

    这样上例中模式串的next数组各值最终应该为:

            图4-1 正确的next数组各值
next数组求解的具体过程如下:
    初始化: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;

         图4-5 第四个数组元素值0
    如果你还是没有弄懂上述过程是怎么一回事,请现在拿出一张纸和一支笔出来,一步一步的画下上述过程。相信我,把图画出来了之后,你一定能明白它的。
    然后,我留一个问题给读者,为什么上述的next数组要那么求?有什么原理么?

    提示:我们从上述字符串abab 各字符的next值-1 0 -1 0,可以看出来,根据求得的next数组值,偷用前缀、后缀的概念,一定可以判断出在abab之中,前缀和后缀相同,即都是ab,反过来,如果一个字符串的前缀和后缀相同,那么根据前缀和后缀依次求得的next各值也是相同的。

  • 5、利用求得的next数组各值运用Kmp算法

    Ok,next数组各值已经求得,万事俱备,东风也不欠了。接下来,咱们就要应用求得的next值,应用KMP算法来匹配字符串了。还记得KMP算法是怎么一回事吗?容我再次引用下之前的KMP算法的代码,如下:

view plain
  1. //代码5-1    
  2. //int kmp_seach(char const*, int, char const*, int, int const*, int pos)  KMP模式匹配函数    
  3. //输入:src, slen主串    
  4. //输入:patn, plen模式串    
  5. //输入:nextval KMP算法中的next函数值数组    
  6. int kmp_search(char const* src, int slen, char const* patn, int plen, int const* nextval, int pos)    
  7. {    
  8.     int i = pos;    
  9.     int j = 0;    
  10.     while ( i < slen && j < plen )    
  11.     {    
  12.         if( j == -1 || src[i] == patn[j] )    
  13.         {    
  14.             ++i;    
  15.             ++j;    
  16.         }    
  17.         else    
  18.         {    
  19.             j = nextval[j];              
  20.             //当匹配失败的时候直接用p[j_next]与s[i]比较,    
  21.             //下面阐述怎么求这个值,即匹配失效后下一次匹配的位置    
  22.         }    
  23.     }    
  24.     if( j >= plen )    
  25.         return i-plen;    
  26.     else    
  27.         return -1;    
  28. }    

我们上面已经求得的next值,如下:

        图5-1 求得的正确的next数组元素各值

    以下是匹配过程,分三步:
    第一步:主串和模式串如下,S[3]与P[3]匹配失败。

               图5-2 第一步,S[3]与P[3]匹配失败
    第二步:S[3]保持不变,P的下一个匹配位置是P[next[3]],而next[3]=0,所以P[next[3]]=P[0],即P[0]与S[3]匹配。在P[0]与S[3]处匹配失败。

                图5-3 第二步,在P[0]与S[3]处匹配失败

    第三步:与上文中第3小节末的情况一致。由于上述第三步中,P[0]与S[3]还是不匹配。此时i=3,j=nextval[0]=-1,由于满足条件j==-1,所以进入循环的if部分,++i=4,++j=0,即主串指针下移一个位置,从P[0]与S[4]处开始匹配。最后j==plen,跳出循环,输出结果i-plen=4(即字串第一次出现的位置),匹配成功,算法结束。

                图5-4 第三步,匹配成功,算法结束
    所以,综上,总结上述三步为

  1. 开始匹配,直到P[3]!=S[3],匹配失败;
  2. nextval[3]=0,所以P[0]继续与S[3]匹配,再次匹配失败;
  3. nextval[0]=-1,满足循环if部分条件j==-1,所以,++i,++j,主串指针下移一个位置,从P[0]与S[4]处开始匹配,最后j==plen,跳出循环,输出结果i-plen=4,算法结束。

第三部分、KMP算法的两种实现

代码实现一:   

    根据上文中第二部分内容的解析,完整写出KMP算法的代码已经不是难事了,如下:

view plain
  1. //copyright@2011 binghu and july  
  2. #include "StdAfx.h"  
  3. #include <string>  
  4. #include <iostream>  
  5. using namespace std;  
  6.   
  7. //代码4-1    
  8. //修正后的求next数组各值的函数代码    
  9. void get_nextval(char const* ptrn, int plen, int* nextval)    
  10. {    
  11.     int i = 0;  //注,此处与下文的代码实现二不同的是,i是从0开始的(代码实现二i从1开始)     
  12.     nextval[i] = -1;    
  13.     int j = -1;    
  14.     while( i < plen-1 )    
  15.     {    
  16.         if( j == -1 || ptrn[i] == ptrn[j] )   //循环的if部分    
  17.         {    
  18.             ++i;    
  19.             ++j;    
  20.             //修正的地方就发生下面这4行    
  21.             if( ptrn[i] != ptrn[j] ) //++i,++j之后,再次判断ptrn[i]与ptrn[j]的关系    
  22.                 nextval[i] = j;      //之前的错误解法就在于整个判断只有这一句。    
  23.             else    
  24.                 nextval[i] = nextval[j];    
  25.         }    
  26.         else                                 //循环的else部分    
  27.             j = nextval[j];    
  28.     }    
  29. }    
  30.   
  31. void print_progress(char const* src, int src_index, char const* pstr, int pstr_index)  
  32. {  
  33.     cout<<src_index<<"\t"<<src<<endl;  
  34.     cout<<pstr_index<<"\t";  
  35.     forint i = 0; i < src_index-pstr_index; ++i )  
  36.         cout<<" ";  
  37.     cout<<pstr<<endl;  
  38.     cout<<endl;  
  39. }  
  40.   
  41. //代码5-1    
  42. //int kmp_seach(char const*, int, char const*, int, int const*, int pos)  KMP模式匹配函数    
  43. //输入:src, slen主串    
  44. //输入:patn, plen模式串    
  45. //输入:nextval KMP算法中的next函数值数组    
  46. int kmp_search(char const* src, int slen, char const* patn, int plen, int const* nextval, int pos)    
  47. {    
  48.     int i = pos;    
  49.     int j = 0;    
  50.     while ( i < slen && j < plen )    
  51.     {    
  52.         if( j == -1 || src[i] == patn[j] )    
  53.         {    
  54.             ++i;    
  55.             ++j;    
  56.         }    
  57.         else    
  58.         {    
  59.             j = nextval[j];              
  60.             //当匹配失败的时候直接用p[j_next]与s[i]比较,    
  61.             //下面阐述怎么求这个值,即匹配失效后下一次匹配的位置    
  62.         }    
  63.     }    
  64.     if( j >= plen )    
  65.         return i-plen;    
  66.     else    
  67.         return -1;    
  68. }    
  69.   
  70. int   main()  
  71. {  
  72.     std::string src = "aabcabcebafabcabceabcaefabcacdabcab";  
  73.     std::string prn = "abac";  
  74.   
  75.     int* nextval = new int[prn.size()];  
  76.     //int* next = new int[prn.size()];  
  77.     get_nextval(prn.data(), prn.size(), nextval);  
  78.     //get_next(prn.data(), prn.size(), next);  
  79.   
  80.     forint i = 0; i < prn.size(); ++i )  
  81.         cout<<nextval[i]<<"\t";  
  82.     cout<<endl;  
  83.       
  84.     cout<<"result sub str: "<<src.substr( kmp_search(src.data(), src.size(), prn.data(), prn.size(), nextval, 0) )<<endl;  
  85.     system("pause");  
  86.   
  87.     delete[] nextval;  
  88.     return 0;  
  89. }   

    运行结果,如下图所示:

代码实现二

     再给出代码实现二之前,让我们再次回顾下关于KMP算法的第一篇文章中的部分内容

第二节、KMP算法

2.1、 覆盖函数(overlay_function)

    覆盖函数所表征的是pattern本身的性质,可以让为其表征的是pattern从左开始的所有连续子串的自我覆盖程度。比如如下的字串,abaabcaba

    可能上面的图令读者理解起来还是不那么清晰易懂,其实很简单,针对字符串abaabcaba

a(-1) b(-1)a(0) a0 b(1) c(-1) a(0) b(1)a(2)

解释:

  1. 初始化为-1  
  2. b与a不同为-1   
  3. 与第一个字符a相同为0   
  4. 还是a为0   
  5. 后缀ab与前缀ab两个字符相同为1 
  6. 前面并无前缀c为-1  
  7. 与第一个字符同为0  
  8. 后缀ab前缀ab为1 
  9. 前缀aba后缀aba为2

    由于计数是从0始的,因此覆盖函数的值为0说明有1个匹配,对于从0还是从来开始计数是偏好问题,具体请自行调整,其中-1表示没有覆盖,那么何为覆盖呢,下面比较数学的来看一下定义,比如对于序列

  a0a1...aj-1 aj

要找到一个k,使它满足

  a0a1...ak-1ak=aj-kaj-k+1...aj-1aj

    而没有更大的k满足这个条件,就是说要找到尽可能大k,使pattern前k字符与后k字符相匹配,k要尽可能的大,原因是如果有比较大的k存在。

    但若我们选择较小的满足条件的k,那么当失配时,我们就会使pattern向右移动的位置变大,而较少的移动位置是存在匹配的,这样我们就会把可能匹配的结果丢失。比如下面的序列,

    在红色部分失配,正确的结果是k=1的情况,把pattern右移4位,如果选择k=0,右移5位则会产生错误。计算这个overlay函数的方法可以采用递推,可以想象如果对于pattern的前j个字符,如果覆盖函数值为k

    a0a1...ak-1ak=aj-kaj-k+1...aj-1aj
则对于pattern的前j+1序列字符,则有如下可能
    ⑴     pattern[k+1]==pattern[j+1] 此时overlay(j+1)=k+1=overlay(j)+1
    ⑵     pattern[k+1]≠pattern[j+1] 此时只能在pattern前k+1个子符组所的子串中找到相应的overlay函数,h=overlay(k),如果此时pattern[h+1]==pattern[j+1],则overlay(j+1)=h+1否则重复(2)过程.

下面给出一段计算覆盖函数的代码:

view plain
  1. //copyright@ staurman  
  2. //updated@2011 July  
  3. #include "StdAfx.h"  
  4. #include<iostream>  
  5. #include<string>  
  6. using namespace std;  
  7.   
  8. //solve to the next array  
  9. void compute_overlay(const string& pattern)  
  10. {  
  11.     const int pattern_length = pattern.size();  
  12.     int *overlay_function = new int[pattern_length];  
  13.     int index;  
  14.     overlay_function[0] = -1;  
  15.     for(int i=1;i<pattern_length;++i)      
  16.         //注,与上文代码段一不同的是,此处i是从1开始的,所以,下文中运用俩种方法求出来的next数组各值会有所不同  
  17.     {  
  18.         index = overlay_function[i-1];  
  19.         //store previous fail position k to index;  
  20.   
  21.         while(index>=0 && pattern[i]!=pattern[index+1])  
  22.         {  
  23.             index = overlay_function[index];  
  24.         }  
  25.         if(pattern[i]==pattern[index+1])  
  26.         {  
  27.             overlay_function[i] = index + 1;    
  28.         }  
  29.         else  
  30.         {  
  31.             overlay_function[i] = -1;  
  32.         }  
  33.     }  
  34.     for(int i=0;i<pattern_length;++i)  
  35.     {  
  36.         cout<<overlay_function[i]<<endl;  
  37.     }  
  38.     delete[] overlay_function;  
  39. }  
  40.   
  41. //abaabcaba  
  42. int main()  
  43. {  
  44.     string pattern = "abaabcaba";  
  45.     compute_overlay(pattern);  
  46.     system("pause");  
  47.     return 0;  
  48. }  

    运行结果如下所示:

2.2、kmp算法
     有了覆盖函数,那么实现kmp算法就是很简单的了,我们的原则还是从左向右匹配,但是当失配发生时,我们不用把target_index向回移动,target_index前面已经匹配过的部分在pattern自身就能体现出来,只要动pattern_index就可以了。

当发生在j长度失配时,只要把pattern向右移动j-overlay(j)长度就可以了。

     如果失配时pattern_index==0,相当于pattern第一个字符就不匹配,这时就应该把target_index加1,向右移动1位就可以了。

    ok,下图就是KMP算法的过程(红色即是采用KMP算法的执行过程):

    (另一作者saturnman发现,在上述KMP匹配过程图中,index=8和index=11处画错了。还有,anaven也早已发现,index=3处也画错了。非常感谢。但图已无法修改,见谅)

KMP 算法可在O(n+m)时间内完成全部的串的模式匹配工作。

    OK,下面此前写的关于KMP算法的第一篇文章中的源码:

view plain
  1. //copyright@ saturnman  
  2. //updated@ 2011 July  
  3. #include "stdafx.h"  
  4. #include<iostream>  
  5. #include<string>  
  6. #include <vector>  
  7. using namespace std;  
  8.   
  9. int kmp_find(const string& target,const string& pattern)  
  10. {  
  11.     const int target_length=target.size();  
  12.     const int pattern_length=pattern.size();  
  13.     int* overlay_value=new int[pattern_length];  
  14.     overlay_value[0]=-1;        //remember:next array's first number was -1.  
  15.     int index=0;  
  16.   
  17.     //next array  
  18.     for (int i=1;i<pattern_length;++i)  
  19.         //注,此处的i是从1开始的  
  20.     {  
  21.         index=overlay_value[i-1];  
  22.         while (index>=0 && pattern[index+1]!=pattern[i])  //remember:!=  
  23.         {  
  24.             index=overlay_value[index];  
  25.         }  
  26.         if(pattern[index+1] == pattern[i])  
  27.         {  
  28.             overlay_value[i]=index+1;  
  29.         }  
  30.         else  
  31.         {  
  32.             overlay_value[i]=-1;  
  33.         }  
  34.     }  
  35.   
  36.     //mach algorithm start  
  37.     int pattern_index=0;  
  38.     int target_index=0;  
  39.     while (pattern_index<pattern_length && target_index<target_length)  
  40.     {  
  41.         if (target[target_index] == pattern[pattern_index])  
  42.         {  
  43.             ++target_index;  
  44.             ++pattern_index;  
  45.         }   
  46.         else if(pattern_index==0)  
  47.         {  
  48.             ++target_index;  
  49.         }  
  50.         else  
  51.         {  
  52.             pattern_index=overlay_value[pattern_index-1]+1;  
  53.         }  
  54.     }  
  55.     if (pattern_index==pattern_length)  
  56.     {  
  57.         return target_index-pattern_index;  
  58.     }   
  59.     else  
  60.     {  
  61.         return -1;  
  62.     }  
  63.     delete [] overlay_value;  
  64. }  
  65.   
  66. int main()  
  67. {  
  68.     string sourc="ababc";  
  69.     string pattern="abc";  
  70.     cout<<kmp_find(sourc,pattern)<<endl;  
  71.     system("pause");  
  72.     return 0;  
  73. }  

    由于是abc跟ababc匹配,那么将返回匹配的位置“2”,运行结果如所示:

第四部分、测试

    针对上文中第三部分的两段代码测试了下,纠结了,两种求next数组的方法对同一个字符串求next数组各值,得到的结果竟然不一样,如下二图所示:

    1、两种方法对字符串abab求next数组各值比较:

    2、两种对字符串abaabcaba求next数组各值比较:

    为何会这样呢,其实很简单,上文中已经有所说明了,代码实现一的i 是从0开始的,代码实现二的i 是从1开始的。但从最终的运行结果来看,暂时还是以代码实现段二为准。

第五部分、KMP完整准确源码

    求next数组各值的方法为:

view plain
  1. //copyright@ staurman  
  2. //updated@2011 July  
  3. #include "StdAfx.h"  
  4. #include<iostream>  
  5. #include<string>  
  6. using namespace std;  
  7.   
  8. //solve to the next array  
  9. void compute_overlay(const string& pattern)  
  10. {  
  11.     const int pattern_length = pattern.size();  
  12.     int *overlay_function = new int[pattern_length];  
  13.     int index;  
  14.     overlay_function[0] = -1;  
  15.     for(int i=1;i<pattern_length;++i)  
  16.     {  
  17.         index = overlay_function[i-1];  
  18.         //store previous fail position k to index;  
  19.   
  20.         while(index>=0 && pattern[i]!=pattern[index+1])  
  21.         {  
  22.             index = overlay_function[index];  
  23.         }  
  24.         if(pattern[i]==pattern[index+1])  
  25.         {  
  26.             overlay_function[i] = index + 1;    
  27.         }  
  28.         else  
  29.         {  
  30.             overlay_function[i] = -1;  
  31.         }  
  32.     }  
  33.     for(int i=0;i<pattern_length;++i)  
  34.     {  
  35.         cout<<overlay_function[i]<<endl;  
  36.     }  
  37.     delete[] overlay_function;  
  38. }  
  39.   
  40. //abaabcaba  
  41. int main()  
  42. {  
  43.     string pattern = "abaabcaba";  
  44.     compute_overlay(pattern);  
  45.     system("pause");  
  46.     return 0;  
  47. }  

    运行结果入下图所示:abab的next数组各值是-1,-1,0,1,而非本文第二部分所述的-1,0,-1,0。为什么呢?难道是搬石头砸了自己的脚?

    NO,上文第四部分末已经详细说明,上处代码i 从0开始,本文第二部分代码i 从1开始。

    KMP算法完整源码,如下:

view plain
  1. //copyright@ saturnman  
  2. //updated@ 2011 July  
  3. #include "stdafx.h"  
  4. #include<iostream>  
  5. #include<string>  
  6. #include <vector>  
  7. using namespace std;  
  8.   
  9. int kmp_find(const string& target,const string& pattern)  
  10. {  
  11.     const int target_length=target.size();  
  12.     const int pattern_length=pattern.size();  
  13.     int* overlay_value=new int[pattern_length];  
  14.     overlay_value[0]=-1;        //remember:next array's first number was -1.  
  15.     int index=0;  
  16.   
  17.     //next array  
  18.     for (int i=1;i<pattern_length;++i)  
  19.         //注,此处的i是从1开始的  
  20.     {  
  21.         index=overlay_value[i-1];  
  22.         while (index>=0 && pattern[index+1]!=pattern[i])    
  23.         {  
  24.             index=overlay_value[index];  
  25.         }  
  26.         if(pattern[index+1] == pattern[i])  
  27.         {  
  28.             overlay_value[i]=index+1;  
  29.         }  
  30.         else  
  31.         {  
  32.             overlay_value[i]=-1;  
  33.         }  
  34.     }  
  35.   
  36.     //mach algorithm start  
  37.     int pattern_index=0;  
  38.     int target_index=0;  
  39.     while (pattern_index<pattern_length && target_index<target_length)  
  40.     {  
  41.         if (target[target_index] == pattern[pattern_index])  
  42.         {  
  43.             ++target_index;  
  44.             ++pattern_index;  
  45.         }   
  46.         else if(pattern_index==0)  
  47.         {  
  48.             ++target_index;  
  49.         }  
  50.         else  
  51.         {  
  52.             pattern_index=overlay_value[pattern_index-1]+1;  
  53.         }  
  54.     }  
  55.     if (pattern_index==pattern_length)  
  56.     {  
  57.         return target_index-pattern_index;  
  58.     }   
  59.     else  
  60.     {  
  61.         return -1;  
  62.     }  
  63.     delete [] overlay_value;  
  64. }  
  65.   
  66. int main()  
  67. {  
  68.     string sourc="ababc";  
  69.     string pattern="abc";  
  70.     cout<<kmp_find(sourc,pattern)<<endl;  
  71.     system("pause");  
  72.     return 0;  
  73. }  

    运行结果如下:

第六部分、一眼看出字符串的next数组各值

    上文已经用程序求出了一个字符串的next数组各值,接下来,稍稍演示下,如何一眼大致判断出next数组各值,以及初步判断某个程序求出的next数组各值是不是正确的。有一点务必注意:下文中的代码全部采取代码实现二,即i是从1开始的

  • 1、对字符串aba求next数组各值,各位可以先猜猜,-1,...,aba中,a初始化为-1,第二个字符b与a不同也为-1,最后一个字符和第一个字符都是a,所以,我猜其next数组各值应该是-1,-1,0,结果也不出所料,如下图所示:

  • 2、字符串“abab”呢,不用猜了,我已经看出来了,当然上文中代码实现一和代码实现二都已经求出来了。如果i 是1开始的话,那么next数组各值将如代码实现二所运行的那样,将是:-1,-1,0,1;
  • 3、字符串“abaabcaba”呢,next数组如上第三部分代码实现二所述,为-1,-1,0,0,1,-1,0,1,2;
  • 4、字符串“abcdab”呢,next数组各值将是-1,-1,-1,-1,0,1;
  • 5、字符串“abcdabc”呢,next数组各值将是-1,-1,-1,-1,0,1,2;
  • 6、字符串“abcdabcd”呢,那么next数组各值将是-1,-1,-1,-1,0,1,2,3;

    怎么样,看出规律来了没?呵呵,可以用上述第五部分中求next数组的方法自个多试探几次,相信,很快,你也会跟我一样,不用计算,一眼便能看出某个字符串的next数组各值了。如此便恭喜你,理解了next数组的求法,KMP算法也就算是真真正正彻彻底底的理解了。完。

相关链接

  1. KMP之第二篇文章:六(续)、从KMP算法一步一步谈到BM算法。
  2. KMP之第一篇文章:六、教你初步了解KMP算法、updated。

后记 

     相信,看过此文后,无论是谁,都一定可以把KMP算法搞懂了(但万一还是有读者没有搞懂,那怎么办呢?还有最后一个办法:把本文打印下来,再仔细琢磨。如果是真真正正想彻底弄懂某一个东西,那么必须付出些代价。但万一要是打印下来了却还是没有弄懂呢?那来北京找我吧,我手把手教你。祝好运)。
    在结束全文之前,谈两点感悟:
  1. 语言->数据结构->算法:语言是基础,够啃一辈子,基本的常见的数据结构得了如指掌,最后才是算法。除了算法之外,有更多更重要且更值得学习的东西(最重要的是,学习如何编程)。切勿盲目跟风,找准自己的兴趣点,和领域才是关键。这跟选择职位、与领域并持久做下去,比选择公司更重要一样。选择学什么东西不重要,重要的是你的兴趣。
  2. 修订这篇文章之时,个人接触KMP都有一年了,学算法也刚好快一年。想想阿,我弄一个KMP,弄了近一年了,到今天才算是真正彻底理解其思想,可想而知,当初创造这个算法的k、m、p三人是何等不易。我想,有不少读者是因为我的出现而想学算法的,但不可急功近利,切勿妄想算法速成。早已说过,学算法先修心。
     OK,文中有关任何问题或错误,烦请不吝赐教与指正。谢谢,完。
    July、二零一一年十二月五日中午。

KMP算法,是由Knuth,Morris,Pratt共同提出的模式匹配算法,其对于任何模式和目标序列,都可以在线性时间内完成匹配查找,而不会发生退化,是一个非常优秀的模式匹配算法。但是相较于其他模式匹配算法,该算法晦涩难懂,第一次接触该算法的读者往往会看得一头雾水,主要原因是KMP算法在构造跳转表next过程中进行了多个层面的优化和抽象,使得KMP算法进行模式匹配的原理显得不那么直白。本文希望能够深入KMP算法,将该算法的各个细节彻底讲透,扫除读者对该算法的困扰。

KMP算法对于朴素匹配算法的改进是引入了一个跳转表next[]。以模式字符串abcabcacab为例,其跳转表为:

j 1 2 3 4 5 6 7 8 910pattern[j]abcabcacabnext[j]0110110501跳转表的用途是,当目标串target中的某个子部target[m...m+(i-1)]与pattern串的前i个字符pattern[1...i]相匹配时,如果target[m+i]与pattern[i+1]匹配失败,程序不会像朴素匹配算法那样,将pattern[1]与target[m+1]对其,然后由target[m+1]向后逐一进行匹配,而是会将模式串向后移动i+1 - next[i+1]个字符,使得pattern[next[i+1]]与target[m+i]对齐,然后再由target[m+i]向后与依次执行匹配。

举例说明,如下是使用上例的模式串对目标串执行匹配的步骤

 1 2 3 4 5 6 7 8 91011121314151617181920212223242526babcbabcabcaabcabcabcacabcabcabcacab                 abcabcacab                    abcabcacab                   abcabcacab                    abcabcacab                   abcabcacab 通过模式串的5次移动,完成了对目标串的模式匹配。这里以匹配的第3步为例,此时pattern串的第1个字母与target[6]对齐,从6向后依次匹配目标串,到target[13]时发现target[13]='a',而pattern[8]='c',匹配失败,此时next[8]=5,所以将模式串向后移动8-next[8] = 3个字符,将pattern[5]与target[13]对齐,然后由target[13]依次向后执行匹配操作。在整个匹配过程中,无论模式串如何向后滑动,目标串的输入字符都在不会回溯,直到找到模式串,或者遍历整个目标串都没有发现匹配模式为止。

next跳转表,在进行模式匹配,实现模式串向后移动的过程中,发挥了重要作用。这个表看似神奇,实际从原理上讲并不复杂,对于模式串而言,其前缀字符串,有可能也是模式串中的非前缀子串,这个问题我称之为前缀包含问题。以模式串abcabcacab为例,其前缀4 abca,正好也是模式串的一个子串abc(abca)cab,所以当目标串与模式串执行匹配的过程中,如果直到第8个字符才匹配失败,同时也意味着目标串当前字符之前的4个字符,与模式串的前4个字符是相同的,所以当模式串向后移动的时候,可以直接将模式串的第5个字符与当前字符对齐,执行比较,这样就实现了模式串一次性向前跳跃多个字符。所以next表的关键就是解决模式串的前缀包含。当然为了保证程序的正确性,对于next表的值,还有一些限制条件,后面会逐一说明。

如何以较小的代价计算KMP算法中所用到的跳转表next,是算法的核心问题。这里我们引入一个概念f(j),其含义是,对于模式串的第j个字符pattern[j],f(j)是所有满足使pattern[1...k-1] = pattern[j-(k-1)...j - 1](k < j)成立的k的最大值。还是以模式串abcabcacab为例,当处理到pattern[8] = 'c'时,我们想找到'c'前面的k-1个字符,使得pattern[1...k-1] = pattern[8-(k-1)...7],这里我们可以使用一个笨法,让k-1从1到6递增,然后依次比较,直到找到最大值的k为止,比较过程如下

k-1前缀关系子串1a==a2ab!=ca3abc!=bca4abca==abca5abcab!=cabca6abcabc!=bcabca因为要取最大的k,所以k-1=1不是我们要找的结果,最后求出k的最大值为4+1=5。但是这样的方法比较低效,而且没有充分利用到之前的计算结果。在我们处理pattern[8] = 'c'之前,pattern[7] = 'a'的最大前缀包含问题已经解决,f(7) = 4,也就是说,pattern[4...6] = pattern[1...3],此时我们可以比较pattern[7]与pattern[4],如果pattern[4]=pattern[7],对于pattern[8]而言,说明pattern[1...4]=pattern[4...7],此时,f(8) = f(7) + 1 = 5。再以pattern[9]为例,f(8) = 5,pattern[1...4]=pattern[4...7],但是pattern[8] != pattern[5],所以pattern[1...5]!=pattern[4...8],此时无法利用f(8)的值直接计算出f(9)。

j 1 2 3 4 5 6 7 8 910pattern[j]abcabcacabnext[j]0110110501f(j)0111234512我们可能考虑还是使用之前的笨方法来求出f(9),但是且慢,利用之前的结果,我们还可以得到更多的信息。还是以pattern[8]为例。f(8) = 5,pattern[1...4]=pattern[4...7],此时我们需要关注pattern[8],如果pattern[8] != pattern[5],那么在匹配算法如果匹配到pattern[8]才失败,此时就可以将输入字符target[n]与pattern[f(8)] = pattern[5]对齐,再向后依次执行匹配,所以此时的next[8] = f(8)(此平移的正确性,后面会作出说明)。而如果pattern[8] = pattern[5],那么pattern[1...5]=pattern[4...8]如果target[n]与pattern[8]匹配失败,那么同时也意味着target[n-5...n]!=pattern[4...8],那么将target[n]与pattern[5]对齐,target[n-5...n]也必然不等于pattern[1...5],此时我们需要关注f(5) = 2,这意味着pattern[1] = pattern[4],因为pattern[1...4]=pattern[4...7],所以pattern[4]=pattern[7]=pattern[1],此时我们再来比较pattern[8]与pattern[2],如果pattern[8] != pattern[2],就可以将target[n]与pattern[2],然后比较二者是否相等,此时next[8] = next[5] = f(2)。如果pattern[8] = pattern[2],那么还需要考察pattern[f(2)],直到回溯到模式串头部为止。下面给出根据f(j)值求next[j]的递推公式:

如果 pattern[j] != pattern[f(j)],next[j] = f(j);

如果 pattern[j] = pattern[f(j)],next[j] = next[f(j)];

当要求f(9)时,f(8)和next[8]已经可以得到,此时我们可以考察pattern[next[8]],根据前面对于next值的计算方式,我们知道pattern[8] != pattern[next[8]]。我们的目的是要找到pattern[9]的包含前缀,而pattern[8] != pattern[5],pattern[1...5]!=pattern[4...8]。我们继续考察pattern[next[5]]。如果pattern[8] = pattern[next[5]],假设next[5] = 3,说明pattern[1...2] = pattern[6...7],且pattern[3] = pattern[8],此时对于pattern[9]而言,就有pattern[1...3]=pattern[6...8],我们就找到了f(9) = 4。这里我们考察的是pattern[next[j]],而不是pattern[f(j)],这是因为对于next[]而言,pattern[j] != pattern[next[j]],而对于f()而言,pattern[j]与pattern[f(j)]不一定不相等,而我们的目的就是要在pattern[j] != pattern[f(j)]的情况下,解决f(j+1)的问题,所以使用next[j]向前回溯,是正确的。

现在,我们来总结一下next[j]和f(j)的关系,next[j]是所有满足pattern[1...k - 1] = pattern[(j - (k - 1))...j -1](k < j),且pattern[k] != pattern[j]的k中,k的最大值。而f(j)是满足pattern[1...k - 1] = pattern[(j - (k - 1))...j -1](k < j)的k中,k的最大值。还是以上例的模式来说,对于第7个元素,其f(j) = 4, 说明pattern[7]的前3个字符与模式的前缀3相同,但是由于pattern[7] = pattern[4], 所以next[7] != 4。

通过以上这些,读者可能会有疑问,为什么不用f(j)直接作为KMP算法的跳转表呢?实际从程序正确性的角度讲是可以的,但是使用next[j]作为跳转表更加高效。还是以上面的模式为例,当target[n]与pattern[7]发生匹配失败时,根据f(j),target[n]要继续与pattern[4]进行比较。但是在计算f(8)的时候,我们会得出pattern[7] = pattern[4],所以target[n]与pattern[4]的比较也必然失败,所以target[n]与pattern[4]的比较是多余的,我们需要target[n]与更小的pattern进行比较。当然使用f(j)作为跳转表也能获得不错的性能,但是KMP三人将问题做到了极致。

我们可以利用f(j)作为媒介,来递推模式的跳转表next。算法如下:

[cpp] view plaincopy
  1. inline void BuildNext(const char* pattern, size_t length, unsigned int* next)  
  2. {  
  3.     unsigned int i, t;  
  4.   
  5.     i = 1;  
  6.     t = 0;  
  7.     next[1] = 0;  
  8.   
  9.     while(i < length + 1)  
  10.     {  
  11.         while(t > 0 && pattern[i - 1] != pattern[t - 1])  
  12.         {  
  13.             t = next[t];  
  14.         }  
  15.   
  16.         ++t;  
  17.         ++i;  
  18.   
  19.         if(pattern[i - 1] == pattern[t - 1])  
  20.         {  
  21.             next[i] = next[t];  
  22.         }  
  23.         else  
  24.         {  
  25.             next[i] = t;  
  26.         }  
  27.     }  
  28.   
  29.     //pattern末尾的结束符控制,用于寻找目标字符串中的所有匹配结果用  
  30.     while(t > 0 && pattern[i - 1] != pattern[t - 1])  
  31.     {  
  32.         t = next[t];  
  33.     }  
  34.   
  35.     ++t;  
  36.     ++i;  
  37.   
  38.     next[i] = t;  
  39. }  

程序中,9到27行的循环需要特别说明一下,我们发现在循环开始之后,就没有再为t赋新值,也就是说,对于计算next[j]时的t值,在计算next[j+1]时,还会用得着。实际这时的t的就等于f(j)。还是以上例的目标串为例,当j等于1,我们可以得出t = f(2) = 1。使用归纳法,当计算完next[j]后,我们假设此时t=f(j),此时第11~14行的循环就是要找到满足pattern[k] = pattern[j]的最大k值。如果这样的k存在,对于pattern[j+1]而言,其前k个元素,与模式的前缀k相同。此时的t+1就是f(j+1)。这时我们就要判断pattern[j+1]和pattern[t](t = t+1)的关系,然后求出next[j+1]。这里需要初始条件next[1] = 0。

利用跳转表实现字符串匹配的算法如下:

[cpp] view plaincopy
  1. unsigned int KMP(const char* text, size_t text_length, const char* pattern, size_t pattern_length, unsigned int* matches)  
  2. {  
  3.     unsigned int i, j, n;  
  4.     unsigned int next[pattern_length + 2];  
  5.   
  6.     BuildNext(pattern, pattern_length, next);  
  7.   
  8.     i = 0;  
  9.     j = 1;  
  10.     n = 0;  
  11.   
  12.     while(pattern_length + 1 - j <= text_length - i)  
  13.     {  
  14.         if(text[i] == pattern[j - 1])  
  15.         {  
  16.             ++i;  
  17.             ++j;  
  18.   
  19.             //发现匹配结果,将匹配子串的位置,加入结果  
  20.             if(j == pattern_length + 1)  
  21.             {  
  22.                 matches[n++] = i - pattern_length;  
  23.                 j = next[j];  
  24.             }  
  25.         }  
  26.         else  
  27.         {  
  28.             j = next[j];  
  29.   
  30.             if(j == 0)  
  31.             {  
  32.                 ++i;  
  33.                 ++j;  
  34.             }  
  35.         }  
  36.     }  
  37.   
  38.     //返回发现的匹配数  
  39.     return n;  
  40. }  

该算法在原有基础上进行了扩展,在原模式串末尾加入了一个“空字符”,“空字符”不等于任何的可输入字符,当目标串匹配至“空字符”时,说明已经在目标字符串中发现了模式,将模式串在目标串中的位置,加入matchs[]数组中,同时判定为匹配失败,并根据“空字符”的next值,跳转到适当位置,这样算法就可以识别出字符串中所有的匹配子串。

最后,对KMP算法的正确性做一简要说明,还是以上文的模式串pattern和目标串target为例,假设已经匹配到第3部的位置,且在target[13]处发现匹配失败,我们如何决定模式串的滑动步数,来保证既要忽略不必要的多余比较,又不漏过可能的匹配呢?

  1 2 3 4 5 6 7 8 91011121314151617181920212223242526targetbabcbabcabcaabcabcabcacabcpattern     abcabcacab           

对于例子中的情况,显然向后移动多于3个字符有可能会漏过target[9...18]这样的的可能匹配。但是为什么向后移动1个或者2个字符是不必要的多余比较呢?当target[13]与pattern[8]匹配失败时,同时也意味着,target[6...12] = pattern[1...7],而next[8]=5,意味着,pattern[1...4] = pattern[4...7],pattern[1...5] != pattern[3...7],pattern[1...6] != pattern[2...7]。如果我们将模式串后移1个字符,使pattern[7]与target[13]对齐,此时target[7...12]相当于pattern[2...7],且target[7...12]与pattern[1..6]逐个对应,而我们已经知道pattern[1...6] != pattern[2...7]。所以不管target[13]是否等于pattern[7],此次比较都必然失败。同理向前移动2个字符也是多余的比较。由此我们知道当在pattern[j]处发生匹配失败时,将当前输入字符与pattern[j]和pattern[next[j]]之间的任何一个字符对齐执行的匹配尝试都是必然失败的。这就说明,在模式串从目标串头移动到目标串末尾的过程中,除了跳过了必然失败的情况之外,没有漏掉任何一个可能匹配,所以KMP算法的正确性是有保证的。

后记:

  • 首先要感谢Knuth-Morris-Pratt那篇光辉的论文《Fast Pattern Matching In Strings》,让我们在字符串处理的道路上看得更远。本文的例子和思路,均完全来自这篇论文,论文后面还对KMP算法的时间复杂度进行了彻底的分析。
  • KMP算法是一个高度优化的精妙算法,所以初涉该算法的时候,不要指望一蹴而就,一下子就将KMP算法理解透,而是应该循序渐进,逐步加深理解。据说该算法是Knuth,Morris,Pratt三人分别独立发现的,我斗胆揣测一下该算法的演进历程。首先应该是发现了模式串前缀的自包含问题,然后是提出了f(j)的概念,然后是搞定了如何计算f(j),然后提出了next[j]的概念,然后搞定了如何用f(j)计算next[j+1],然后是只用f(j)做中间结果直接算出next[j+1]。之所以我会这么猜测,主要是因为next跳转表的概念和生成算法太高端,中间经历了多个转换,极难一步到位想出来这么搞。所以我们也应该按照这个流程来学习KMP算法,而如何计算f(j)则是整个算法的精髓所在。
  • 实际上,KMP算法中所用到的跳转表next是一个简化了的DFA,对于DFA而言,其跳转和输入的字符集有关,而KMP算法中的跳转表,对于模式串中的当前位置j-1,只有两种跳转方式pattern[j],和^pattern[j],所以KMP算法的跳转功能要弱于DFA,但是其构建速度,又大大快于DFA,在花费较小代价的同时,取得了逼近DFA的效果。下面是对于文中使用的模式串生成跳转表(上)和DFA的比较,显然DFA要复杂的多(这个是我手画的如果有画错的地方,请读者不吝赐教)。


0 0
原创粉丝点击