KMP 算法是用来查找子串在字符串中的初始位置的算法
在寻找子串在字符串中的初始位置时,如果是采用暴力破解,每次匹配到不合适的,那么都需要回到子串的开头进行逐一匹配,时间复杂度很高,那么可以利用kmp来进行优化,让子串不需要每次都回到开头匹配
KMP 算法算法就利用之前判断过的信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过 next 数组找到,前面匹配过的位置,省去了大量的计算时间。
比如 aabaa 这个子串,我们在比较到第五个字符也就是最后一个a时,发现不匹配,那么我们就可以查找前面的子串的next值,这个next可以找到前缀一样的(我们回退的时候,第四个字符a跟第一个是一样的,那么第一个就不需要比较了,直接比较第二个字符--这些信息在next数组存储维护)
什么是最长公共前后缀
1️⃣ 字符串的前缀是指不包含最后一个字符的所有以第一个字符(索引为0)开头的连续子串
比如字符串 “ABABA” 的前缀有:A,AB,ABA,ABAB
2️⃣ 字符串的后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
比如字符串 “ABABA” 的后缀有:BABA,ABA,BA,A
3️⃣ 公共前后缀:一个字符串的 所有前缀连续子串 和 所有后缀连续子串 中相等的子串
比如字符串 “ABABA”
- 前缀有:A,AB,ABA,ABAB
- 后缀有:BABA,ABA,BA,A
因此公共前后缀有:A ,ABA
4️⃣ 最长公共前后缀:所有公共前后缀 的 长度最长的 那个子串
比如字符串 “ABABA” ,公共前后缀有:A ,ABA
由于
ABA
是 三个字符长度,A
是一个字符长度,那么最长公共前后缀就是 ABA
📝 再比如说一个字符串 str = “ABCABD”
- 对于str从
索引为0
开始的子串 “A
” 而言:
-
- 前缀:不包含
最后一个字符A
的 所有以第一个字符A
开头 的 连续子串 不存在 - 后缀:不包含
第一个字符A
的 所有以最后一个字符A
结尾 的 连续子串 不存在
- 前缀:不包含
- 因此该子串的最长公共前后缀 为 0
- 对于str从 索引为0 开始的子串 “
AB
” 而言:
-
- 前缀:不包含
最后一个字符B
的 所有以第一个字符A
开头 的 连续子串 有 —— “A” - 后缀:不包含
第一个字符A
的 所有以最后一个字符B
结尾 的 连续子串 有 —— “B”
- 前缀:不包含
- 因此该子串的最长公共前后缀 为 0
- 对于str从 索引为0 开始的子串 “
ABC
” 而言:
-
- 前缀:不包含
最后一个字符C
的 所有以第一个字符A
开头 的 连续子串 有 —— “A”,“AB” - 后缀:不包含
第一个字符A
的 所有以最后一个字符C
结尾 的 连续子串有 —— “BC”,“C”
- 前缀:不包含
- 前缀与后缀的连续子串不存在相同的,因此该子串的最长公共前后缀 为 0
- 对于str从 索引为0 开始的子串 “
ABCA
” 而言:
-
- 前缀:不包含
最后一个字符A
的 所有以第一个字符A
开头 的 连续子串 有 —— “A”,“AB”,“ABC” - 后缀:不包含
第一个字符A
的 所有以最后一个字符A
结尾 的 连续子串有 —— “BCA”,“CA”,“A”
- 前缀:不包含
- 前缀与后缀的连续子串中存在相同且最长的子串 A,因此该子串的最长公共前后缀 为 1
- 对于str从 索引为0 开始的子串 “
ABCAB
” 而言:
-
- 前缀:不包含
最后一个字符B
的 所有以第一个字符A
开头 的 连续子串 有 —— “A”,“AB”,“ABC”,“ABCA” - 后缀:不包含
第一个字符A
的 所有以最后一个字符B
结尾 的 连续子串有 —— “BCAB”,“CAB”,“AB”,“B”
- 前缀:不包含
- 前缀与后缀的连续子串中存在相同且最长的子串 AB,因此该子串的最长公共前后缀 为 2
- 对于str从 索引为0 开始的子串 “
ABCABD
” 而言:
-
- 前缀:不包含
最后一个字符D
的 所有以第一个字符A
开头 的 连续子串 有 —— “A”,“AB”,“ABC”,“ABCA”,“ABCAB” - 后缀:不包含
第一个字符A
的 所有以最后一个字符D
结尾 的 连续子串有 —— “BCABD”,“CABD”,“ABD”,“BD”,“D”
- 前缀:不包含
- 前缀与后缀的连续子串不存在相同的,因此该子串的最长公共前后缀 为 0
什么是部分匹配表Next
对于字符串str,从 第一个字符开始的每个子串 的 最后一个字符 与 该子串的最长公共前后缀的长度 的对应关系表格。这个表我们以 int[] next
数组方式进行存储。
比如说上面举的例子:
- 子串 “
A
”:最后一个字符是 A,该子串的最长公共前后缀长度是 0,因此对应关系就是 A - 0 - 子串 “
AB
”:最后一个字符是 B,该子串的最长公共前后缀长度是 0,因此对应关系就是 B - 0 - 子串 “
ABC
”:最后一个字符是 C,该子串的最长公共前后缀长度是 0,因此对应关系就是 C - 0 - 子串 “
ABCA
”:最后一个字符是 A,该子串的最长公共前后缀长度是 1,因此对应关系就是 A - 1 - 子串 “
ABCAB
”:最后一个字符是 B,该子串的最长公共前后缀长度是 2,因此对应关系就是 B - 2 - 子串 “
ABCABD
”:最后一个字符是 D,该子串的最长公共前后缀长度是 0,因此对应关系就是 D - 0
因此综上,我们说该字符串 str 的部分匹配表为:
A | B | C | A | B | D |
0 | 0 | 0 | 1 | 2 | 0 |
那么对应的next数组就是 int[] next = {0, 0, 0, 1, 2, 0}
完整Java代码如下
package org.example;
class Solution {
public static void main(String[] args) {
Solution solution = new Solution();
System.out.println(solution.strStr("hello", "ll"));
}
public int strStr(String haystack, String needle) {
if(needle.length() == 0) return 0;
int[] next = new int[needle.length()];
getNext(needle, next);
//遍历haystack开始匹配
int j = 0;
for(int i = 0; i < haystack.length(); i++) {
//不等于j变动
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;
}
private void getNext(String needle, int[] next) {
//获取最长前缀,记录不匹配时跳转的位置
int j = 0; next[j] = 0;
for(int i = 1; i < needle.length(); i++) {
//找到匹配的或者j为0了
while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
j = next[j-1];
}
//如果是因为匹配了,那么j++
if(needle.charAt(i) == needle.charAt(j)) {
j++;
}
//记录i
next[i] = j;
}
}
}