首页 > 代码库 > KMP算法详解 --从july那学的

KMP算法详解 --从july那学的

KMP代码:

 1     int KmpSearch(char* s, char* p)   2     {   3         int i = 0;   4         int j = 0;   5         int sLen = strlen(s);   6         int pLen = strlen(p);   7         while (i < sLen && j < pLen)   8         {   9             //①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      10             if (j == -1 || s[i] == p[j])  11             {  12                 i++;  13                 j++;  14             }  15             else  16             {  17                 //②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]      18                 //next[j]即为j所对应的next值        19                 j = next[j];  20             }  21         }  22         if (j == pLen)  23             return i - j;  24         else  25             return -1;  26     }  

 

拿例子来说,当S[10]跟P[6]匹配失败时,KMP不是简单的如朴素匹配那样把模式串右移一位,而是执行第②条指令:“如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]”,即j 从6变到2(后面我们将求得P[6],即字符D对应的next 值为2),所以相当于模式串向右移动的位数为j - next[j]位(j - next[j] = 6-2 = 4位)。

    向右移动4位后,S[10]跟P[2]继续匹配。为什么要向右移动4位呢,因为移动4位后,模式串中又有个“AB”可以继续跟S[8]S[9]匹配,相当 于在模式串中找相同的前缀和后缀,然后根据前缀后缀求出next 数组,最后基于next 数组进行匹配(不关心next 数组怎么求来的,只想看匹配过程是咋样的,可直接跳到下文3.3.4节)。

 

(1)next数组是针对pattern的,就是模式例如给文本S:BBC_ABCDAB_ABCDABCDABDE   P:ABCDABD.

next数组是针对p的,

(2)求next数组,就是求出最长前缀后缀,然后左移一位,第一个为-1.

  • ①寻找前缀后缀最长公共元素长度
    • 对于Pj = p0 p1 ...pj-1,寻找模式串Pj中长度最大且相等的前缀和后缀
      • 即寻找满足条件的最大的k,使得p0 p1 ...pk-1 = pj-k pj-k+1...pj-1。也就是说,k是模式串中各个子串的前缀后缀的公共元素的长度,所以求最大的k,就是看某个子串的哪个前缀后缀的公共元素最多。注意:前缀都是从头开始找的,不能是中间。
      • 举个例子,如果给定的模式串为“abaabcaba”,那么它的各个子串的前缀后缀的公共元素的最大长度值如下表格所示:
  • ②求next数组
    • 根据第①步骤中求得的各个前缀后缀的公共元素的最大长度求得next 数组,相当于前者右移一位且初值赋为-1,如下表格所示:
  • ③匹配失配,模式串向右移动的位数为:j - next[j]。换言之,当模式串的后缀pj-k pj-k+1...pj-1 跟文本串 si-k si-k+1, ..., si-1失配时,j = next[j],根据next 数组得到next[j] = k,从而让模式串的前缀p0 p1 ...pk-1继续跟文本串 si-k si-k+1, ..., si-1匹配。
    • 注:j 是模式串中失配字符的位置,且 j 从0开始计数。
    综上,KMP的next 数组相当于告诉我们:当模式串中的某个字符跟文本串中的某个字符匹配失配时,模式串下一步应该跳到哪个位置。如模式串中在j 处的字符跟文本串在i 处的字符匹配失配时,下一步用next [j] 处的字符继续跟文本串匹配,相当于模式串向右移动 j - next[j] 位。
 
(3)接下来,咱们来写代码求下next 数组。

    基于之前的理解,可知计算next 数组的方法可以采用递推:

  • 1. 如果对于值k,已有p0 p1, ..., pk-1 = pj-k pj-k+1, ..., pj-1,相当于next[j] = k
    • 此意味着什么呢?究其本质,next[j] = k 代表p[j] 之前的模式串子串中,有长度为k 的相同前缀和后缀。有了这个next 数组,在KMP匹配中,当模式串后缀中j 处的字符失配时,模式串向右移动j - next[j] 位。

举个例子,如下图,根据模式串“ABCDABD”的next 数组可知失配位置的字符D对应的next 值为2,代表字符D前有长度为2的相同前缀和后缀(这个相同的前缀后缀即为“AB”),失配后,模式串需要向右移动j - next [j] = 6 - 2 =4位。

向右移动4位后,模式串中的字符C继续跟文本串匹配。

  • 2. 下面的问题是:已知next [0, ..., j],如何求出next [j + 1]呢?

    对于pattern的前j+1个序列字符:

  • 若pattern[k] == pattern[j],则next[j + 1 ] = next [j] + 1 = k + 1;
  • 若pattern[k ] ≠ pattern[j],如果此时pattern[ next[k] ] == pattern[j ],则next[ j + 1 ] =  next[k] + 1,否则继续递归重复此过程。 相当于在字符p[j+1]之前不存在长度为k+1的前缀"p0 p1, …, pk-1 pk"跟后缀“pj-k pj-k+1, …, pj-1 pj"相等,那么是否可能存在另一个值t+1 < k+1,使得长度更小的前缀 “p0 p1, …, pt-1 pt” 等于长度更小的后缀 “pj-t pj-t+1, …, pj-1 pj” 呢?如果存在,那么这个t+1 便是next[ j+1]的值,此相当于利用next 数组进行P串前缀跟P串后缀的匹配。
   一般的文章或教材可能就此一笔带过,但大部分的初学者可能还是不能很好的理解上述求解next 数组的原理,故接下来,我再来着重说明下。
    如下图所示,假定给定模式串ABCDABCE,且已知next [j] = k(相当于“p0 pk-1” = “pj-k pj-1” = AB,可以看出k为2),现要求next [j + 1]等于多少?因为pk = pj = C,所以next[j + 1] = next[j] + 1 = k + 1(可以看出next[j + 1] = 3)。代表字符E前的模式串中,有长度k+1 的相同前缀后缀。
这里是根据P[j]==P[k]==C,得出n[j+1]=n[j]+1=k+1=3,k=2,j从0开始
    但如果pk != pj 呢?说明“p0 pk-1 pk”  ≠ “pj-k pj-1 pj”。换言之,当pk != pj后,字符E前有多大长度的相同前缀后缀呢?很明显,因为C不同于D,所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:next[j + 1] = next[j] + 1 。所以,咱们只能去寻找长度更短一点的相同前缀后缀。

    结合上图来讲,若能在前缀“ p0 pk-1 pk ” 中不断的递归k = next [k],找到一个字符pk’ 也为D,代表pk’ = pj,且满足p0 pk‘-1 pk‘ = pj-k‘ pj-1 pj,则最大相同的前缀后缀长度为k‘ + 1,从而next [j + 1] = k’ + 1 = next [k‘ ] + 1。否则前缀中没有D,则代表没有相同的前缀后缀,next [j + 1] = 0。(其实也是KMP开始查找的思想了
    所以,因最终在前缀ABC中没有找到D,故E的next 值为0:

模式串的后缀:ABDE
模式串的前缀:ABC
前缀右移两位:     ABC

    此外,咱们还可以换个角度思考这个问题:(其实也是KMP开始查找的思想了)
  1. 类似KMP的匹配思路,当p0 p1, ..., pj 跟主串s0 s1, ..., si匹配时,如果模式串在j处失配,则j = next [j],相当于模式串需要向右移动j - next[j] 位。
  2. 现在前缀“p0 pk-1 pk”  去跟后缀 “pj-k pj-1 pj”匹配,发现在pk处匹配失败,那么前缀需要向右移动多少位呢?根据已经求得的前缀各个字符的next 值,可得前缀应该向右移动k - next[k]位,相当于k = next[k]。
    • 若移动之后,pk‘ = pj,则代表字符E前存在长度为next[ k‘ ] + 1的相同前缀后缀;
    • 否则继续递归k = next [k],直到pk’’ 跟pj匹配成功,或者不存在任何k(0 < k < j)满足pk = pj ,且 k = next[k] = -1停止递归。
    综上,可以通过递推求得next 数组,代码如下所示:
 1     void GetNext(char* p,int next[])   2     {   3         int pLen = strlen(p);   4         next[0] = -1;   5         int k = -1;   6         int j = 0;   7         while (j < pLen - 1)   8         {   9             //p[k]表示前缀,p[j]表示后缀  10             if (k == -1 || p[j] == p[k])   11             {  12                 ++j;  13                 ++k;  14                 next[j] = k;  15             }  16             else   17             {  18                 k = next[k];  19             }  20         }  21     }  

(4)next数组优化

Next 数组的优化

   行文至此,咱们全面了解了暴力匹配的思路、KMP算法的原理、流程、流程之间的内在逻辑联系,以及next 数组的简单求解(《最大长度表》整体右移一位,然后初值赋为-1)和代码求解,最后基于《next 数组》的匹配,看似洋洋洒洒,清晰透彻,但以上忽略了一个小问题。

    比如,如果用之前的next 数组方法求模式串“abab”的next 数组,可得其next 数组为-1 0 0 1(0 0 1 2整体右移一位,初值赋为-1),当它跟下图中的文本串去匹配的时候,发现b跟c失配,于是模式串右移j - next[j] = 3 - 1 =2位。

    右移2位后,b又跟c失配。事实上,因为在上一步的匹配中,已经得知p[3] = b,与s[3] = c失配,而右移两位之后,让p[ next[3] ] = p[1] = b 再跟s[3]匹配时,必然失配。问题出在哪呢?

    问题出在不该出现p[j] = p[ next[j] ]。为什么呢?理由是:

  • p[j] != s[i] 时,下次匹配必然是p[ next [j]] 跟s[i]匹配,如果p[j] = p[ next[j] ],必然导致后一步匹配失败,所以不能允许p[j] = p[ next[j ]]
    • 因为p[j]已经跟s[i]失配,然后你还用跟p[j]等同的值p[next[j]]去跟s[i]匹配,很显然,必然失配。

    所以,咱们得修改下求next 数组的代码。

 

 1     //优化过后的next 数组求法   2     void GetNextval(char* p, int next[])   3     {   4         int pLen = strlen(p);   5         next[0] = -1;   6         int k = -1;   7         int j = 0;   8         while (j < pLen - 1)   9         {  10             //p[k]表示前缀,p[j]表示后缀    11             if (k == -1 || p[j] == p[k])  12             {  13                 ++j;  14                 ++k;  15                 //较之前next数组求法,改动在下面4行  16                 if (p[j] != p[k])  17                     next[j] = k;   //之前只有这一行  18                 else  19                     //因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]  20                     next[j] = next[k];  21             }  22             else  23             {  24                 k = next[k];  25             }  26         }  27     }  

(5)优化版next如何迅速找到

可 能有些读者会问:原始next 数组是前缀后缀最长公共元素长度值右移一位, 然后初值赋为-1而得,那么优化后的next 数组如何快速心算出呢?实际上,只要求出了原始next 数组,那么可根据原始next 数组快速求出优化后的next 数组。还是以abab为例,如下表格所示:

    

(6)KMP算法时间复杂度

咱们先来回顾下KMP匹配算法的流程:

KMP的算法流程:

  • 假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
    • 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
    • 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。

    我们发现如果某个字符匹配成功,模式串首字符的位置保持不动,仅仅是i++、j++;如果匹配失配,i 不变(即 i 不回溯),模式串会跳过匹配过的next [j]个字符。整个算法最坏的情况是,当模式串首字符位于i - j的位置时才匹配成功,算法结束。
    所以,如果文本串的长度为n,模式串的长度为m,那么匹配过程的时间复杂度为O(n),算上计算next的O(m)时间,KMP的整体时间复杂度为O(m + n)。

KMP算法详解 --从july那学的