KMP 算法
KMP 算法
假设我们在字符串 s 中查找是否有字符串 p(长度分别为 n, m),设整数 i, j 分别为字符串 s, p 中的游标。我们会发现暴力法每次匹配失败,i, j 都会回退,其中 i 的步进为 1,j 则每次都回到 0。KMP 算法的主要思想就是让 i 不回退,只有 j 往回移动,因此只要能限制 j 移动的次数就可以确保时间复杂度够低。要保证 i 不回退,我们需要知道 j 该回退的步长,这就要求我们有额外的信息来源。
假设我们有一个长度为 n 的整型数组 next,其中 next[i] 表示 s[0] ~ s[i] 的字符串中相同的前缀以及后缀的最大长度。以 s = "ababcabcd" 为例:
i |
前缀 | 后缀 | 最大长度 |
|---|---|---|---|
0 |
无 | 无 | 0 |
1 |
"a" |
"b" |
0 |
2 |
"a", "ab" |
"a", "ba" |
1 |
3 |
"a", "ab", "aba" |
"b", "ab", "bab" |
2 |
4 |
"a", "ab", "aba", "abab" |
"c", "ba", "abc", "babc" |
0 |
5 |
"a", "ab", "aba", "abab", "ababc" |
"a", "ca", "bca", "abca", "babca" |
1 |
6 |
... | ... | 2 |
7 |
... | ... | 0 |
8 |
... | ... | 0 |
这样,假如字符串 p = "ababa",那么在第一次匹配时,在 i = j = 4 时会失败,但这时 i 就不用回到 1 再开始匹配,因为 next 数组已经告诉它中间有一段你匹配不上,直接跳到可以匹配的部分就好,二者匹配的部分就是前后缀重复的部分(s 前后缀相同 + p 前缀和 s 前缀相同 = p 前缀和 s 后缀相同),所以 i 不用回到 1,待在原地就好,而 j 也不用回到 0,而是回到 2( p 前缀 "ab" 和 s 后缀 "ab" 相同)。
有了 next 数组我们可以发现,i, j 共有三种情况:
i, j同时自增1(字符匹配成功)i不变,j回退(p非首字符匹配失败)i自增1,j不变(p首字符匹配失败)
简单分析我们可以发现,j 最多自增 min(n, m) 次,所以 j 最多回退 min(n, m) 次,也就是说 j 最多变化 2*min(n, m) 次,时间复杂度是线性的。
生成 next 我们使用和上方类似的思想,将它自己和自己匹配就可以找出相同的前后缀。依旧使用 i, j 作为游标,i 自增时更新 next,而 j 回退时用到的 next 在之前已经逐步计算出来了(动态规划),所以时间复杂度和上面的算法相仿(代码写出来都几乎一样),为 O(n),那么整体的时间复杂度就是 O(n+m)。
Java 代码
public class KMP{
public void search(String s, String p) {
int[] next = this.getNext(s);
int i = 0, j = 0, len = s.length();
while (i < len) {
if (s.charAt() == p.charAt(j)) {
++i; ++j;// 不能像 C 一样写成 ++i, ++j; 就要多个括号,不开心
} else if (j != 0)
j = next[j - 1]
else
++i;
}
}
public int[] getNext(String s) {
int len = s.length();
int[] next = new int[len];
next[0] = 0;
int i = 1, j = 0;
while (i < len) {
if (s.charAt(i) == s.charAt(j))
// 对比 search,多了给 next 赋值
next[i++] = ++j;
else if (j != 0)
// 对比 search 没变化
j = next[j - 1];
else
// 对比 search,多了给 next 赋值
next[i++] = 0;
if (j == len)
System.out.println(i - j + 1);
}
return next;
}
}
个人博客:https://wilfredshen.cn/

浙公网安备 33010602011771号