假设我们在字符串 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)
。
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://www.cnblogs.com/wilfredshen/p/13583335.html