算法训练营第九天| KMP算法

92 阅读3分钟

KMP 算法

KMP 算法是一个快速查找匹配串的算法,它的作用其实就是本题问题:如何快速在「原字符串」中找到「匹配字符串」。

在传统的暴力解法中,在「原字符串」中找到「匹配字符串」需要 O(n * m) 的时间复杂度(假设原字符串长度为n,匹配字符串长度为m),而KMP算法可以在 O(n + m)的时间复杂度内完成这个匹配操作。

在详细介绍 KMP 算法之前,我们需要先了解几个概念:

  • 前缀后缀
  • 最长公共前后缀
  • 前缀表

首先我们先来讲解前缀后缀
前缀是指:一定包含首字母,但是不包含尾字母的所有子串。
后缀是指:一定包含尾字母,但是不包含首字母的所有子串。
假设我们拥有一个字符串aabaaf
他的前缀有:

a, aa, aab, aaba, aabaa

他的后缀有:

f, af, aaf, baaf, abaaf

在了解了前缀后缀之后,我们就可以开始寻找最长相等前后缀了:
还是我们上面的这个例子,从我们上面列出的前缀和后缀我们可以知道,该字符串并没有相等的前缀与后缀,因此他的最长相等前后缀的长度为0

而前缀表,就是将一个字符串从首字母开始,到尾字母为止的所有子串 (注意,不是所有子串,而是所有包含首字母的子串, 也不是前缀子串,因为要包括最后一个字母)最长相等前后缀的长度算出来然后总结成一个数组。
例如我们的例子aabaaf,他所有字串的最长相等前后缀长度如下:

子串前缀后缀最长相等前后缀最长相等前后缀的 长度
a没有前缀没有后缀0
aaaaa1
aaba, aab, ab0
aabaa, aa, aaba, ba, abaa1
aabaaa, aa, aab, aabaa, aa, baa, abaaaa2
aabaafa, aa, aab, aaba, aabaaf, af, aaf, baaf, abaaf0

如上表所示,对于字符串aabaaf我们获得了一个前缀表 {0, 1, 0, 1, 2, 0}。
在 KMP 算法中,我们也将这个前缀表称之为 next 数组。

在了解了以上这些概念之后,我们就可以开始讲解我们的 KMP 算法了:
假设我们的文本串为aabaabaaf,而我们的模式串为aabaaf。 已知我们的前缀表为 {0, 1, 0, 1, 2, 0}。我们可以将这些信息像下图这样展现出来。

image.png

前缀表的这些数字在 KMP 算法中代表的含义可以先简单理解为重新匹配时可以 跳过 匹配的字符数量(跳过模式串的字符)。
当我们在遇到一个不匹配的字符时,假设这个字符的下标为i,那么我们就找前缀表下标为 i - 1 位置,这个位置的数字就代表着我们重新匹配时可以跳过多少个字符。

image.png 如上图所示:
当我们匹配到下标为 5 的字符时,发现 b != f,这时我们观察其前缀子串中的最长相等前后缀为2。也就是子串 aabaa中前缀aa与后缀aa相等。
这时我们知道了在子串aabaa中,一定有一个前缀aa,与我们的后缀aa相等,那么我们就可以将前缀中的aa, 移动到我们后缀中aa的位置(如下图)。
这样,我们就跳过了对前缀aa的对比,直接从模式串的第三位b开始比较。

image.png

如果不跳过,我们就需要进行如下图中的两次比较才能得到我们现在的结果:

image.png

前缀表(next数组)的代码实现:

public void getNext(int[] next, String s) {
    int j = 0;
    next[0] = 0;
    char[] ch = s.toCharArray();
    int n = ch.length;
    for (int i = 1; i < n; ++i) {
        while (j > 0 && ch[i] != ch[j]) {
            j = next[j - 1];
        }
        if (ch[i] == ch[j]) {
            j++;
        }
        next[i] = j;
    } 
}