KMP算法
- KMP 算法可以做到仅仅后移模式串,比较指针不回溯
核心思想
- 查找主串与模式串的最长公共前后缀
- 将模式串中前缀的字符移动到后缀字符的位置
- 继续往后匹配字符。
next数组
- 前缀: 包含首位字符但不包括末位字符的子串
- 后缀: 包含末位字符但不包括首位字符的子串
- 最长公共前后缀
- 当我们分析的字符串中有多个公共前后缀时,我们要取最长的公共前后缀。
- 长度要小于错误位置左端子串长度的公共前后缀。
- next数组
- 当主串与模式串的某一位字符不匹配时,模式串要退回的位置
- 记录最长公共前缀的长度
判断最长公共前缀
- 判断前缀的下一个字符与后缀末尾字符之后的字符进行对比
- 我们要用公共前缀的下一个字符,与我们公共后缀的下一个字符做比对
- 如果它们是一样的,那么我们下一段字符的最长公共前后缀就是 当前最长公共前后缀 + 1
- 定义 j 指向的就是我们公共前缀的末尾字符,定义 i 指向的就是我们公共后缀的末尾字符。
- 当前最长公共前后缀长度 + 1 = j 所在下标 + 1
j : 0 1 2 3 4 5 6 7 8 9 10 11
p : A B A B A A A B A B A A
下标0
A
无最长公共前缀
next[0] = 0
下标1
AB
无最长公共前缀
next[1] = 0
下标2
ABA
最长公共前缀:A
next[2] = 1
下标3
ABAB
最长公共前缀:AB
next[3] = 2
下标4
ABABA
最长公共前缀:ABA
next[4] = 3
下标5
ABABAA
最长公共前缀:A
next[5] = 1
下标6
ABABAAA
最长公共前缀:A
next[6] = 1
下标7
ABABAAAB
最长公共前缀:AB
next[7] = 2
下标8
ABABAAABA
最长公共前缀:ABA
next[8] = 3
下标9
ABABAAABAB
最长公共前缀:ABAB
next[9] = 4
下标10
ABABAAABABA
最长公共前缀:ABABA
next[10] = 5
下标11
ABABAAABABAA
最长公共前缀:ABABAA
next[11] = 6
prefix前缀数组 / PMT 数组
[0,0,1,2,3,1,1,2,3,4,5,6]
prefix数组减去 1 就是 next数组
匹配步骤
- 定义 i j 从下标0开始
- 循环直到 i 到达我们主串的长度
- 匹配成功
- 模式串 中的 j 指向的字符与 原串 中 i 指向的字符相同时
- i 和 j 就都可以往右挪动一位,往后继续匹配了。
- 把公共前缀挪动到公共后缀的位置,j = next [ j ]
- 找主串中的下一个字符与我们模式串的下标 0 的字符进行匹配了,所以这里就是 ++i 即可。
- 最后判断 j 是否到达模式串最后
代码
function KMP(source, str) {
// 定义数组
const next = new Array(str.length).fill(0);
// next数组
{
var i = 1, j = 0;
while (i < str.length - 1) {
// 如果匹配成功 那么最长公共前缀长度 j + 1
if (str[i] === str[j]) {
j++;
i++;
next[i] = j;
}
// 匹配失败 j 指针回溯到 0 位置
else {
if (j > 0) {
j = next[j];
} else {
i++;
}
}
}
// 0号位置不放置字符
next[0] = -1;
console.log(next)
}
// 主串匹配
{
let i = 0,
j = 0;
while (i < source.length) {
if (str[j] === source[i]) {
++j, ++i;
} else {
if (j > 0) {
j = next[j];
} else {
++i;
}
}
if (j === str.length)
return true;
}
return false;
}
}
console.log(KMP('ABABABAABABAAABABAA', 'ABABAAABABAA'));
[
-1, 0, 0, 1, 2,
3, 1, 1, 2, 3,
4, 5
]
true
console.log(KMP('hello', 'll'));
[-1, 0]
true
console.log(KMP('HaHaHa', 'XiXiXi'));
[-1, 0, 0, 1, 2, 3]
false