文章内容选自尚硅谷
参考了视频 「天勤公开课」KMP算法易懂版.
参考了博客 很详尽KMP算法(厉害).
算法理解
该算法的核心点在于怎么求next数组,这一点视频和博客讲的比较清楚了,但是新手肯定有很多误区,导致看不懂算法,现在简单说说新手常见的误区。
博客很详尽KMP算法(厉害).细分了最大长度表和next数组,本文是基于尚硅谷讲解的,尚硅谷中的next数组其实是博客中的最大长度表。故本文的next数组都理解成博客中的最大长度表。
- KMP算法不像暴力匹配一样,一旦不匹配只移动一位,它之所以要移位,是先求出一段字符串的相同前后缀,然后把前缀的位置移到后缀的 位置上去。毕竟前后缀都相同嘛,中间跳过的位置必然是不匹配的。
- KMP算法next数组每一位对应的值,都是从模式字符串首位置0到该索引位置,这个子字符串的最大相同前后缀的位数。这一步必须得理解。有可能next[i] = 2,next[i+1]=0,这是很正常的,因为string(子串)的后缀的最后一位与该子串的前缀的最后位不匹配了。如果next[i+1]=1,此处肯定是找到了原前缀字符串中具有相等前后缀的小前缀字符串。看不懂请多参考博客和视频。这是核心步骤。
- 做KMP匹配时,是先求出 next数组,再进行匹配,即先根据模式串,求出和模式串字符串等长的next数组,把该数组每个值都求出来后,再根据数组进行查表移位。
- 在求字符串的相同前后缀的时候,可不是把所有前后缀一一列出来比较,而是有独特的算法
public static void KMPNext(String dest) {
next = new int[dest.length()];
next[0] = 0;
//此处把i理解成后缀索引,j理解成前缀索引。前后缀索引的比较。
for(int i = 1,j = 0;i < dest.length();i++) {
while(j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j - 1]; //大前后缀不匹配后,转而判断小前后缀是否匹配
}
if(dest.charAt(i) == dest.charAt(j)){
j++;
}
next[i] = j;
}
}
求相同前后缀时,前后缀的匹配是在模式串中一位一位比较的,把i理解成后缀索引,j理解成前缀索引。一旦在模式串中dest.charAt(i) != dest.charAt(j) 即前后缀不匹配时,就要把前缀进行回溯,但并不是直接回溯到0,而是在大前缀中,以大前缀为基础,找大前缀字符串中前后缀相等的小前缀,回溯到小前缀的位置。如果一直不匹配才最终回溯到0位置。
代码演示
package com.atguigu.kmp;
import java.util.Arrays;
public class KMPAlgorithm {
static int[] next ;
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABCEABCDABD";
String str2 = "ABCDABD";
KMPNext(str2);
System.out.println(Arrays.toString(next));
int index = KMPSearch(str1,str2,next);
System.out.println("index = "+index);
}
//在进行KMP匹配的时候,j是不断回溯的,而i是不断+1的,当j的长度==模式串的长度时,匹配成功
public static int KMPSearch(String str1,String str2,int[] next) {
for(int i = 0,j = 0;i < str1.length();i++) {
while(j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j - 1]; //如果不相等,要对模式串的j进行重置
}
if(str1.charAt(i) == str2.charAt(j)) {
j++; //不能把j++提到上面的while里,因为上面的while退出循环的原因,可能是回溯到了0才 退出的。
}
if(j == str2.length()) {
return i - j +1; //如果j不进行重置的话,i会比j小1,i-j+1刚好为0,因为i++在for循环里面,所以j++比i++快一步,理解成i+1 -j
}
}
return -1;
}
public static void KMPNext(String dest) {
next = new int[dest.length()];
next[0] = 0;
//此处把i理解成后缀索引,j理解成前缀索引。前后缀索引的比较。
for(int i = 1,j = 0;i < dest.length();i++) {
while(j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j - 1]; //大前后缀不匹配后,转而判断小前后缀是否匹配
}
if(dest.charAt(i) == dest.charAt(j)){
j++;
}
next[i] = j;
}
}
}
运行结果为
[0, 0, 0, 0, 1, 2, 0]
index = 23