在进行字符串匹配时,KMP算法的特点是:省去了主串与子串不必要的回溯,这也是KMP算法(在主串有较多重复时)更加高效的关键。
在KMP算法中,任何情况下主串都不回溯。
实现思路
获取next数组
next数组是表示在子串的某一个索引位置上,当前字符加上前面的串 所包含的相同前后缀字符串的最大长度字符
如:
0 1 2 3 4
子串:A B A B D
在索引为3的B,前面的字符串包含的前缀有A,AB,ABA
后缀有B,AB,BAB
可以看出最大相同前后缀为"AB",长度为2
索引在B这个位置的next数组值应该为2
在索引为4的D,前面的字符串包含的前缀有A,AB,ABA,ABAB
后缀有D,BD,ABD,BABD
可以看出没有相同的前后缀
索引在D这个位置的next数组值应该为0
用上述方法算出的next数组为:[0, 0, 1, 2, 0]
KMP算法
- 定义一个
i指针
,用于从头开始遍历一遍主串(不回溯) - 定义一个
j指针
,每次匹配子串(回溯) - 如果在一次匹配中
i
和j
指向的元素相同- 就让
i
和j
分别向后移动一位,再继续循环匹配
- 就让
- 如果
i
和j
指向的元素不同- 此时
i
不动,j
回溯-
j
=
n
e
x
t
[
j
−
1
]
j=next[j-1]
j=next[j−1](
j
回溯至next[j-1]
的索引位置上,因为这个索引位置代表前缀等于后缀的最长部分,就不需要把j
回溯到子串首位,去掉了不必要回溯)
-
j
=
n
e
x
t
[
j
−
1
]
j=next[j-1]
j=next[j−1](
- 此时
- 直到
- 循环至主串剩余长度小于子串长度(没找到返回-1)
- 或者
j
在某一次递增后已经达到子串的长度了(因为j
只会在元素相同的时候后移,只要能后移到子串长度,说明已经匹配到了,返回i - 主串.length()
)
代码实现
package com.xiaohuowa.kmp;
import java.util.Arrays;
/**
* @author 小火娃
* @project_name: my_project
* @package_name: com.xiaohuowa.kmp
*/
public class KMP {
public static void main(String[] args) {
String mainString = "好好学习天天向上";
String subString = "习天上";
int index = kmp(mainString, subString);
System.out.println(index);
}
/**
* 通过 KMP 算法算出子串在主串中是否存在
* @param mainString 主串
* @param sunString 子串
* @return 存在返回在主串中的起始索引,不存在返回-1
*/
public static int kmp(String mainString, String sunString){
int[] next = getNextArr(sunString);
// i 表示从主串第一个元素开始的索引(不回溯)
int i = 0;
// j 表示子串中的索引(会回溯)
int j = 0;
// i循环递增下去
while (i < mainString.length()) {
// 如果比较的时候两个字符相同
if (sunString.charAt(j) == mainString.charAt(i)){
// 如果比较都相同,让i和j都后移
j++;
i++;
// 如果j累加之后已经达到子串的长度了,说明已经找到了,返回子串在主串中的开始索引
if (j == sunString.length()) {
return i - sunString.length();
}
// 循环至主串剩余长度小于子串长度(没找到返回-1)
if (mainString.length()-i < sunString.length()) {
return -1;
}
}else{
// 防止下标越界
if (j != 0){
// 如果一个字符不同了,就让 j 回溯至next[j-1]的索引位置
j = next[j - 1];
}else{
// 如果 j 已经在子串的最开头了,就让 i 后移
i++;
}
}
}
return -1;
}
/**
* 获取前缀表
*
* @param str 子串
* @return 返回next数组
*/
public static int[] getNextArr(String str){
int[] next = new int[str.length()];
// 初始化j变量,j代表最长匹配前缀长度(j指向前缀末尾)
int j = 0;
// 初始化next数组的第一位为0
next[0] = 0;
// i指向后缀末尾
for (int i = 1; i < next.length; i++) {
// 如果前缀匹配了,就累加j,并更新next数组的值
if (str.charAt(i) == str.charAt(j)){
j++;
next[i] = j;
continue;
}
// 如果前缀不匹配,且 j 还没退回子串的首位,让 j 根据next数组中前一位的值,退到那个索引位置
while (j>0 && str.charAt(i) != str.charAt(j)){
j = next[j-1];
// 如果前缀匹配了,就累加j,并更新next数组的值
if (str.charAt(i) == str.charAt(j)){
j++;
next[i] = j;
break;
}
}
}
return next;
}
}