zoukankan      html  css  js  c++  java
  • KMP算法小结

     

    KMP算法小结

    主要看了这里,感觉讲的十分的不错,总结一下。

    首先声明要搜索的串为S,设长度为n,要匹配的串为M,设长度为m.

    先考虑暴力的算法,暴力的算法是遍历S的每一个字符,然后从这个字符开始和M串进行匹配。时间复杂度为O(nm).

    怎么在此基础上进行优化?假设现在从某个位置(设为s)开始和M串进行匹配,如果匹配不成功,暴力算法是从这个位置的下一个位置(s+1)进行匹配,直观上来说就是匹配的字符串向后“滑动”了一位。

    image

    图1

    能不能想办法让M向后移动的距离最大化?考虑最好的情况,如果和M匹配的S中的m个字符和M中的字符没有一个相等,那么能向右移动m位;考虑最坏的情况,比如上图,只能移动一位。

    而KMP就是在这里做文章,让M串向后“滑动”的距离最大化。

    image

    图2

    考虑上面的图,M中灰色部分已经和S的灰色部分匹配上了,而灰色部分后一个字符不匹配,则现在M要向后滑动,假设一直向后滑动,直到如图位置又和S再一次匹配上了,那么从这里我们可以得到如下的结论:

    • A段字符串是M的一个前缀。
    • B段字符串是M的一个后缀。
    • A段字符串和B段字符串相等。

    这样,如果暂时不考虑S,只看M的话,假设已经匹配的M的字串(即图中M中灰色部分)为subM,则subM有个【相等】的【前缀】和【后缀】。而且M在遇到不匹配的时候可以直接滑动到使subM的前缀和subM的后缀重合的地方。而M向后滑动的时候,第一次subM的前缀和后缀重合意味着此时这个相等的subM的前缀和后缀的长度是最大的。

    我们的任务就是要寻找subM的最长的前缀和后缀相等的串。

    知道了这一点,离KMP的真谛也就不远了。现在结合这上面的图模拟一下KMP算法的整个流程:

    • 将S串和M串从第一个字符开始匹配;
    • 如果匹配成功,则subM即灰色部分增加;
    • 如果不成功,则M向后滑动使滑动后的subM的前缀和滑动前的subM的后缀重合,再进行匹配,如果还不成功,则再次滑动M,直到匹配成功或者M滑动到X处。如果到了X处,则从M串的起始位置进行匹配。

    从上面的步骤可以知道,KMP的关键就是要知道当S串中的字符和M串中的字符不匹配时,S串要和M串中的哪个字符继续进行匹配。这个就是在利用状态机模型来解释KMP算法时的状态转移.

    KMP是通过一个定义了一个next数组,这个next数组保存了如果S中的字符和M中的字符不匹配时S要和M中的哪个字符重新进行匹配的坐标值。如图2中所示是例子,S中的X位置和M不匹配了,那么S要和M中A段后面的字符进行比较,从图中来看是M向后滑动了。

    换句话说,next[i]总是保存了当M[i]不匹配时要从M[next[i]]处进行匹配,这个M[next[i]]可能会匹配,如果还不匹配?那么可能会在M[next[next[i]]]处匹配了。这里同时隐含着一个信息,就是i之前的一段字符和next[i]之前的一段字符是相同的,也就是M[0…i-1]相等的前缀和后缀。

    现在考虑next[0],next[1]…next[i]都已经知道了,那么图示如下:

    image

    设j=next[i],灰色部分表明这两段字符是相等的,如果i位置的字符和j位置的字符相等,那么next[i+1]=j+1;因为前一段灰色部分和j位置的字符组成的字符串和后一段灰色的与i连接所形成的字符串是相等的。这正是前面对next数组的定义。如果不相等,则要找到从i开始包括i往前的一段字符串与从0开始的一段字符串相等,这样形成相等的前缀和后缀。所幸我们知道next[next[i]]的值,因为next[i]前面的字串也有最长的公共前缀和后缀,而这个公共的前缀与现在i以及往前形成的字串可能相等,这样一直向前找,如果找不到,则说明i位置的字符从来没有在之前出现过。

    这样求出来的next数组其实是从下标1开始的,因为下标0之前是个空串,下标1则对应着M串的第0个字符。我们设next[0]=-1,仅仅是个标志而已,没有什么特殊的含义。

    那么根据前面所述,可以很容易的写出初始化next数组的代码

       1: void kmpGetNext()
       2: {
       3:     int i=0, j=-1;
       4:     b[i]=j;
       5:     while (i<m)
       6:     {
       7:         while (j>=0 && p[i]!=p[j]) j=b[j];
       8:         i++; j++;
       9:         b[i]=j;
      10:     }
      11: }

    知道了next数组的值,则和S串进行匹配则相对简单了,因为如果碰到不匹配的时候去查找next数组即可,直到找出和当前字符匹配的那个字符。如果找不到怎么办?找不到则会得到-1,也就是没有字符和他进行匹配,那么跳过这个字符,直接从下一个字符进行匹配即可。

    代码如下:

       1: void kmpSearch()
       2: {
       3:     int i=0, j=0;
       4:     while (i<n)
       5:     {
       6:         while (j>=0 && t[i]!=p[j]) j=b[j];
       7:         i++; j++;
       8:         if (j==m)
       9:         {
      10:             report(i-j);
      11:             j=b[j];
      12:         }
      13:     }
      14: }

    看到上面的代码,两层循环,貌似这个代码并不是线性的,其实不然。外层循环了n次这个没有问题,关键是里面的while循环,这个循环的次数是多少并不好确定,然而考虑单单考虑j的值的变化,会发现第七行j增加1,而第6行j则减少,可能减少1,可能减少2,可能少的更多,但是j<0时循环就终止了,也就是说j有n次增加的机会,会有多少次减少的机会?或者问j最多减少多少次?j减少的次数最多的时候,就是每次减少1,这样最多的会减少n次,也就是说第六行的循环最多会执行n次。平摊到每个循环,则执行次数为O(1),所以kmpSearch的时间复杂度仍然是线性的O(n),同理,kmpGetNext的时间复杂度为O(m).

  • 相关阅读:
    CodeForces Gym 100500A A. Poetry Challenge DFS
    CDOJ 486 Good Morning 傻逼题
    CDOJ 483 Data Structure Problem DFS
    CDOJ 482 Charitable Exchange bfs
    CDOJ 481 Apparent Magnitude 水题
    Codeforces Gym 100637G G. #TheDress 暴力
    Gym 100637F F. The Pool for Lucky Ones 暴力
    Codeforces Gym 100637B B. Lunch 找规律
    Codeforces Gym 100637A A. Nano alarm-clocks 前缀和
    TC SRM 663 div2 B AABB 逆推
  • 原文地址:https://www.cnblogs.com/xiayong123/p/3717504.html
Copyright © 2011-2022 走看看