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 |
| aa | a | a | a | 1 |
| aab | a, aa | b, ab | 无 | 0 |
| aaba | a, aa, aab | a, ba, aba | a | 1 |
| aabaa | a, aa, aab, aaba | a, aa, baa, abaa | aa | 2 |
| aabaaf | a, aa, aab, aaba, aabaa | f, af, aaf, baaf, abaaf | 无 | 0 |
如上表所示,对于字符串aabaaf我们获得了一个前缀表 {0, 1, 0, 1, 2, 0}。
在 KMP 算法中,我们也将这个前缀表称之为 next 数组。
在了解了以上这些概念之后,我们就可以开始讲解我们的 KMP 算法了:
假设我们的文本串为aabaabaaf,而我们的模式串为aabaaf。
已知我们的前缀表为 {0, 1, 0, 1, 2, 0}。我们可以将这些信息像下图这样展现出来。
前缀表的这些数字在 KMP 算法中代表的含义可以先简单理解为重新匹配时可以 跳过 匹配的字符数量(跳过模式串的字符)。
当我们在遇到一个不匹配的字符时,假设这个字符的下标为i,那么我们就找前缀表下标为 i - 1 位置,这个位置的数字就代表着我们重新匹配时可以跳过多少个字符。
如上图所示:
当我们匹配到下标为 5 的字符时,发现 b != f,这时我们观察其前缀子串中的最长相等前后缀为2。也就是子串 aabaa中前缀aa与后缀aa相等。
这时我们知道了在子串aabaa中,一定有一个前缀aa,与我们的后缀aa相等,那么我们就可以将前缀中的aa, 移动到我们后缀中aa的位置(如下图)。
这样,我们就跳过了对前缀aa的对比,直接从模式串的第三位b开始比较。
如果不跳过,我们就需要进行如下图中的两次比较才能得到我们现在的结果:
前缀表(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;
}
}