详解 KMP 算法

暴力匹配

S1:aabaabsaabaabaabsaabt 与 S2:aabaabsaabt 暴力匹配流程如下。

aabaabsaabaabaabsaabt
^ 从头开始匹配
aabaabsaabt
^
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
          ^
aabaabsaabt
          ^ 发生不匹配
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
 ^ 回退到上次起始位置的下一位置
aabaabsaabt          
^ 从头开始
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
  ^
aabaabsaabt          
 ^ 发生不匹配
------------------------------------------------------------------------ 
aabaabsaabaabaabsaabt
  ^ 回退到上次起始位置的下一位置
aabaabsaabt          
^ 从头开始,并且发生了不匹配          
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
   ^ 回退到上次起始位置的下一位置
aabaabsaabt          
^ 从头开始
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
      ^ 
aabaabsaabt          
   ^ 发生不匹配
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
    ^ 回退到上次起始位置的下一位置
aabaabsaabt          
^ 从头开始
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
     ^ 
aabaabsaabt          
 ^ 发生不匹配
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
      ^ 回退到上次起始位置的下一位置
aabaabsaabt          
^ 从头开始,并且发生了不匹配       
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
       ^ 回退到上次起始位置的下一位置
aabaabsaabt          
^ 从头开始
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
                  ^ 
aabaabsaabt          
           ^ S2 越界,完全匹配成功

S1 与 S2 如果在第 i 位置没匹配上。则 S1 要回退到第 i+1 位置,而 S2 要回退到 0 位置,再重新开始匹配。

问题在于中间有大量计算被浪费,每次回退后的匹配宛如重新开始匹配一样,与上次匹配毫无关联。

前后缀

前缀:串中第一个字符开始,从前往后,最多到整个串倒数第二个字符,所构成的子串。

后缀:串中最后一个字符开始,从后往前,最多到整个串第二个字符,所构成的子串。

也就是前后缀是原来串的真子集,前缀不能包含尾,后缀不能包含头。

abcabcd
      ^

计算 d 之前的串中最长公共前后缀长度

12345
前缀aababcabcaabcab
后缀cbcabccabcbcabc
长度xx3xx

所以最长公共前后缀长度为 3。

next 数组

next 数组每个元素,就是对应原串位置之前的最长公共前后缀长度。

aabaabsaabt 为例,计算 next 数组。

aabaabsaabt
^
之前为空串,规定为 -1
------------------------------------------------------------------------
aabaabsaabt
 ^
"a",前后缀是真子集,所以为 0
------------------------------------------------------------------------
aabaabsaabt
  ^
"aa"
前缀:a
后缀:a
最长:1
------------------------------------------------------------------------
aabaabsaabt
   ^
"aab"
前缀:a aa
后缀:b ab
最长:0
------------------------------------------------------------------------
aabaabsaabt
    ^
"aaba"
前缀:a aa aab
后缀:a ba aba
最长:1
------------------------------------------------------------------------
aabaabsaabt
     ^
"aabaa"
前缀:a aa aab aaba
后缀:a aa baa abaa
最长:2
------------------------------------------------------------------------
aabaabsaabt
      ^
"aabaab"
前缀:a aa aab aaba aabaa
后缀:b ab aab baab abaab
最长:3
------------------------------------------------------------------------
aabaabsaabt
       ^
"aabaabs"
前缀:a aa aab aaba aabaa aabaab
后缀:s bs abs aabs baabs abaabs
最长:0
------------------------------------------------------------------------
aabaabsaabt
        ^
"aabaabsa"
前缀:a aa aab aaba aabaa aabaab aabaabs
后缀:a sa bsa absa aabsa baabsa abaabsa
最长:1
------------------------------------------------------------------------
aabaabsaabt
         ^
"aabaabsaa"
前缀:a aa aab aaba aabaa aabaab aabaabs aabaabsa
后缀:a aa saa bsaa absaa aabsaa baabsaa abaabsaa
最长:2
------------------------------------------------------------------------
aabaabsaabt
          ^
"aabaabsaab"
前缀:a aa aab aaba aabaa aabaab aabaabs aabaabsa aabaabsaa
后缀:b ab aab saab bsaab absaab aabsaab baabsaab abaabsaab
最长:3

从而得到 next 数组

012345678910
原串字符aabaabsaabt
最长公共前后缀长度-10101230123

主串和模式串元素下标从 0 开始,就是上述结果。

如果下标是从 1 开始,则需要为整个 next 数组所有元素 + 1。

加速

利用 next 数组避免无意义的回退,从而加速整个匹配流程。

aabaabsaabaabaabsaabt
          ^
aabaabsaabt
          ^ 发生不匹配,"aabaabsaab",最长公共前后缀为 3
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
          ^ 无需回退
aabaabsaabt          
   ^ 从前缀末尾的下一个元素开始
------------------------------------------------------------------------
aabaabsaabaabaabsaabt
                  ^ 
aabaabsaabt          
           ^ S2 越界,完全匹配成功

在第 i 位置发生不匹配,隐含两个信息:

  1. 0 ~ i-1 是完全匹配。
  2. 只有在第 i 位置不匹配。
aabaabsaabaabaabsaabt
          ^
aabaabsaabt
          ^ 在此处发生不匹配时,意味着前面位置都是匹配的,也就是说除该位置之前的串是一样的

所以在 i 之前的所有元素都无需再次匹配。只需要从 i 位置开始匹配。

又因为 S1 与 S2 之前匹配的 aabaabsaab 是一样的,其中前后缀 aab 是一样的,所以 S2 中的前缀可以跳过。

aabaabsaabaabaabsaabt
          ^
       aabaabsaabt
          ^

假设如果此处还没有匹配上。

aabaabsaabxaabaabsaabt
          ^
       aabaabsaabt
          ^ 发生不匹配,"aab",最长公共前后缀为 0
------------------------------------------------------------------------
aabaabsaabxaabaabsaabt
          ^ 无需回退
          aabaabsaabt          
          ^ 从前缀末尾的下一个元素开始。
            此时还是不匹配,之前为空串,所以换个开头再次匹配。
------------------------------------------------------------------------
aabaabsaabxaabaabsaabt
           ^ 换个开头再次匹配
           aabaabsaabt          
           ^ 
------------------------------------------------------------------------
aabaabsaabxaabaabsaabt
                      ^ 
           aabaabsaabt          
                      ^ S2 越界,完全匹配成功

所以相当于没匹配上,根据 next 数组的值选择 S2 的起始位置,如果是 -1 那么久 S1 就换下一个元素开始匹配。

代码

// 原始暴力做法,O(N*M)
int bf(char s[], char p[], int n, int m) {
    int i = 0, j = 0;
   	while (i < n && j < m) {
        if (s[i] == p[j]) i++, j++;
        else i = i - j + 1, j = 0;
    }
    return j == m ? i - j : -1;
}

// KMP,O(N + M)
int kmp(char s[], char p[], int n, int m) {
    int ne[m];
    ne[0] = -1, ne[1] = 0;
    for (int i = 2, j = 0; i < m;) { // 求 next
        if (p[i - 1] == s[j]) ne[i++] = ++j;
        else if (j > 0) j = ne[j];
        else ne[i++] = 0;
    }
    
    int i = 0, j = 0;
   	while (i < n && j < m) {
        if (s[i] == p[j]) i++, j++;
        else if (ne[j] == -1) i++;
        else j = ne[j];
    }
    return j == m ? i - j : -1;
}

真题演练

2018 年 408 真题:主串 abaabaabacacaabaabcc 模式串 abaabc,使用 KMP 进行匹配,第一次失配时,i = j = 5,则下次匹配时 ij 的值分别是多少?

abaabaabacacaabaabcc
     ^ 第六次匹配失败,i = 5
abaabc
     ^ "abaab",最长公共前后缀为 2
------------------------------------------------------------------------
abaabaabacacaabaabcc
     ^ 无需回退,i = 5
     abaabc
       ^ 从前缀末尾的下一个元素开始。第三个元素,j = 2

答:i = 5,j = 2

2019 年 408 真题:主串 abaabaabcabaabc 模式串 abaabc,使用 KMP 进行匹配,直到完全匹配一共需要比较多少次?

abaabaabcabaabc
     ^ 第六次匹配失败
abaabc
     ^ "abaab",最长公共前后缀为 2
------------------------------------------------------------------------
abaabaabcabaabc
     ^ 
     abaabc
       ^ 
------------------------------------------------------------------------
abaabaabcabaabc
         ^ 
     abaabc
           ^ 再匹配四次后越界,完成整个匹配

答:6 + 4 = 10 次

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值