1、next 数组的定义
next 数组(前缀表)是在 KMP 算法中使用到的,用于匹配模式串相同前后缀长度
它可以减少匹配次数,其原理是将模式串中每个子串的相同前后缀长度记录下来,当在文本串中匹配失败时,就根据前缀表——即 next 数组——找到模式串中匹配失败前一个字符的位置所对应的前缀尾字符,将模式串的指针移动到该字符
过程说明:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
文本串 | a | a | b | a | a | b | a | a | f |
模式串 | a | a | b | a | a | f | |||
next 数组 | 0 | 1 | 0 | 1 | 2 | 0 |
- 当文本串和模式串的指针都指向 下标5 时,发现不匹配
- 查询到前缀表对应位置的 下标5 前一个值为 2
- 将模式串的指针移动到 下标2
- 继续匹配
2、next 数组的生成
next 数组有三种形式,其原理都是一样的,但是三种形式会导致 KMP 的具体实现发生变化
首先要统一一个认知:前缀指的是不含尾字符的子串,后缀指的是不含首字符的子串
而前缀表中存放的是从模式串首字符到以当前下标为尾字符的子串中,最长的相同前后缀长度
模式串 | a | a | b | a | a | f |
---|---|---|---|---|---|---|
子串1 | a | |||||
子串1最长相同前后缀 | 0 | |||||
子串2 | a | a | ||||
子串2最长相同前后缀 | 1 | |||||
子串3 | a | a | b | |||
子串3最长相同前后缀 | 0 | |||||
子串4 | a | a | b | a | ||
子串4最长相同前后缀 | 1 | |||||
子串5 | a | a | b | a | a | |
子串5最长相同前后缀 | 2 | |||||
子串6 | a | a | b | a | a | f |
子串6最长相同前后缀 | 0 |
综上得出,模式串 “aabaaf” 的 next 数组 [0, 1, 0, 1, 2, 0]
3、next 数组的形式
上面说到 next 数组有四种形式,不同的形式会导致具体的 KMP 算法实现有所不同
- 数组各值为子串最长前后缀的长度
- 在1的基础上,整体右移一位,首位补 -1
- 在1的基础上,各值减一
- 在2的基础上,整体加一
第一种就是最基础的前缀表形式,其缺点明显:在取值时需要取当前下标前一个的值,并且无法区分首元素
第二种的目的就是解决上面两个缺点
第三种也是为了解决上面第一种的两个缺点,实现比第二种简单一点
第四种是为了适配下标从1开始的串(首字符存串的长度)
KMP 具体实现
第一种形式
public class KMP_1 {
private static int[] getNext(char[] needleChars) {
// 基础形式,不对前缀表进行优化
// 生成 next 数组
int[] next = new int[needleChars.length];
// 1. 初始化
// j 指向前缀的尾字符
int j = 0;
next[0] = j;
// i 指向后缀的尾字符,从 1 开始
for (int i = 1; i < needleChars.length; i++) {
// 2. 处理前后缀不相同的情况
while (j > 0 && needleChars[i] != needleChars[j]) {
j = next[j - 1];
}
// 3. 处理前后缀相同的情况
if (needleChars[i] == needleChars[j]) {
j++;
}
next[i] = j;
}
return next;
}
public static int strStr(String haystack, String needle) {
int[] next = getNext(needle.toCharArray());
// i为文本串指针,j为模式串指针
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 - j + 1;
}
}
return -1;
}
}
第二种形式
/**
* 前缀表第二种形式
* 整体右移一位,首位补-1
*/
public class KMP_2 {
private static int[] getNext(char[] needleChars) {
if (needleChars.length <= 1) {
return new int[] {-1};
}
// 第二种形式,整体右移一位,首位补-1
// 生成 next 数组
int[] next = new int[needleChars.length];
// 1. 初始化
next[0] = -1;
next[1] = 0;
// j 指向前缀的尾字符
int j = 0;
// i 指向后缀的尾字符,从 1 开始
for (int i = 1; i < needleChars.length - 1; i++) {
// 2. 处理前后缀不相同的情况
while (j > 0 && needleChars[i] != needleChars[j]) {
j = next[j];
}
// 3. 处理前后缀相同的情况
if (needleChars[i] == needleChars[j]) {
j++;
}
next[i + 1] = j;
}
return next;
}
public static int strStr(String haystack, String needle) {
int[] next = getNext(needle.toCharArray());
// i为文本串指针,j为模式串指针
int j = 0;
for (int i = 0; i < haystack.length(); i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = next[j];
}
if (haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if (j == needle.length()) {
return i - j + 1;
}
}
return -1;
}
}
第三种形式
/**
* 前缀表第三种形式
* 各位减一
*/
public class KMP_3 {
private static int[] getNext(char[] needleChars) {
// 第三种形式,各位减一
// 生成 next 数组
int[] next = new int[needleChars.length];
// 1. 初始化
next[0] = -1;
// j 指向前缀的尾字符
int j = -1;
// i 指向后缀的尾字符,从 1 开始
for (int i = 1; i < needleChars.length; i++) {
// 2. 处理前后缀不相同的情况
while (j >= 0 && needleChars[i] != needleChars[j + 1]) {
j = next[j];
}
// 3. 处理前后缀相同的情况
if (needleChars[i] == needleChars[j + 1]) {
j++;
}
next[i] = j;
}
return next;
}
public static int strStr(String haystack, String needle) {
int[] next = getNext(needle.toCharArray());
// i为文本串指针,j为模式串指针
int j = -1;
for (int i = 0; i < haystack.length(); i++) {
while (j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)) {
j = next[j];
}
if (haystack.charAt(i) == needle.charAt(j + 1)) {
j++;
}
if (j == needle.length() - 1) {
return i - j;
}
}
return -1;
}
}
第四种不做解释
时间复杂度:O(n + m)
空间复杂度:O(m)