简介
KMP 全称为:Knuth-Morris-Pratt,即为Knuth、Morris 和 Pratt 三人发明的算法,其基本思想是在文本串匹配中,当出现字符不匹配时,利用已匹配的模式字符串,避免从头再去做匹配,从而提高效率。 那KMP提高了多少效率呢?设n为文本串长度,m为模式串长度,则暴力匹配的时间复杂度为 O(n * m),而KMP只有 O(n + m)。
一些概念
正式理解KMP算法前,先了解一些概念
前辍:不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串。
最长相同前后辍:一个字符串中,所有相等的前辍与后辍中,最长的那一个。
next[]:回退表,用于处理模式表回退。 它记录了文本串与模式串不匹配的时候,模式串应该从哪里开始重新匹配,由最长相等前后辍的长度计算得出。
为什么需要KMP?
是不是很懵?上来一堆概念直接给整劝退了,上面这些概念,能记多少记多少,之后会有示例,示例解释过程中,需要反复回看以上概念。 请看以下示例:
- 文本串为:ABAAAABAAAAAAAAA
- 模式串为:BAAAAAAAAA
- 前辍有:B、BA、BAA、BAAA、BAAAA、BAAAAA、BAAAAAA、BAAAAAAA、BAAAAAAAA
- 后辍有:A、AA、AAA、AAAA、AAAAA、AAAAAA、AAAAAAA、AAAAAAAA、AAAAAAAAA
- 最长相同前后辍:无(上述前辍和后辍中,无相同前后辍,更无最长相同前后辍)
- i 为:文本串从0开始的索引
我们可以看到暴力匹配下做了很多无用功,我们文本串已经跑过i + 1个字符了,却还要不停重复 “再试” 之前跑过的字符,这时,能不重复跑吗?怎么做?
能。我们跑过了i个字符,因为文本串第i + 1个字符与模式串第x个字符不匹配了,暴力匹配需要回退文本串的指针,此时,我们可以不回退文本串的指针,只回退模式串的指针,但怎样回退呢?这里把回退的索引存到数组next[]中,比如:模式串BAAAAAAAAA的回退表next[]值为:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0](此数组索引为k),表示:当文本串第i + 1个字符与模式串第x个字符不匹配时,模式串需要回退到索引值next[k]处,这里回退表里元素next[k]全是0,即全部回退到模式串首部,直接用文本串第i + 1个字符与模式串索引值为0的字符(即第一个字符),继续比较即可。
那么问题又来了,那为什么这么回退就可以了呢?这个就和前后辍有关了,前后辍相同时,可以在文本串第i + 1个字符与模式串第x个字符匹配失败时,告知模式串,前面有多少个字符不用重复“再试”,已经匹配成功了。 那么怎么计算回表next[]呢?请看下一节。
如何计算回退表next[]
计算方式如下:
- next[] (回退表)长度与模式串长度相同。(毕竟为模式串服务)
- 每个索引数值为:在模式串中,下标i之前(包括i)的字符串中,最长相同前缀后缀的长度。
“在模式串中,下标i之前(包括i)的字符串中,最长相同前缀后缀的长度。”
这句话多少有点难理解,所以这里做个示例:
设有模式串:AAABAA,当i = 2时,此时的字符串为AAA
前辍有:A、AA
后辍有:A、AA
最长相同前后辍为:AA,长度为2
故,当前 next[2] = 2
把所有的 next[] 填完为:[0, 1, 2, 0, 1, 2],即为回退表
代码实现为:
/**
* 获取当前模式串的 next[] (回退表)
* 这里没有右移一位,如果需要右移,所有值减1即可
*/
public void getNext(int[] next, String s){
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
// j要保证大于0,因为下面有取j-1作为数组下标的操作
while (j > 0 && s[i] != s[j]) {
// 注意这里,是要找前一位的对应的回退位置了
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
时间复杂度分析
文本串长度为n,模式串长度为m。因为在匹配的过程中,虽然模式串根据回退表不断调整匹配的位置,但整个文本串每个字符,只会比较一次,故时间复杂度为:O(n)。此前还要单独计算next[](回退表),该算法也只遍历了一次,故时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。而暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大的提高了搜索的效率。
实战
接下来用KMP做一道leetcode上的 “简单题”
28. 实现 strStr()
代码实现为:
/**
* KMP算法,用KMP算法可以提高效率到O(n + m)
*/
class Solution {
/**
* 获取当前模式串的 next[] (回退表)
* 这里没有右移一位,如果需要右移,所有值减1即可
*/
public void getNext(int[] next, String s){
int j = 0;
next[0] = 0;
for(int i = 1; i < s.length(); i++) {
// j要保证大于0,因为下面有取j-1作为数组下标的操作
while (j > 0 && s.charAt(i) != s.charAt(j)) {
// 注意这里,是要找前一位的对应的回退位置了
j = next[j - 1];
}
if (s.charAt(i) == s.charAt(j)) {
j++;
}
next[i] = j;
}
}
public int strStr(String haystack, String needle) {
if(needle.length()==0){
return 0;
}
int[] next = new int[needle.length()];
getNext(next, needle);
int j = 0;
for(int i = 0; i < haystack.length(); i++){
while(j>0 && haystack.charAt(i) != needle.charAt(j)){
j = next[j - 1];
}
if(haystack.charAt(i) == needle.charAt(j)){
j++;
}
if(j == needle.length() ){
return (i - needle.length() + 1);
}
}
return -1;
}
}