kmp算法是字符串问题中常见算法,主要用于字符串的匹配。整体思路上面比较绕,今天让我们一起彻底搞懂kmp算法。
基本概念
1.前缀与后缀
前缀:必须从第0个字符开始,到最后一个字符之前任意一个字符,所构成的子串,都叫做该字符串的前缀
后缀:从字符串的第2个字符以后(包括第二个)任意一个字符,到最后一个字符所构成的子串,都叫做该字符串的后缀
2.相等的前后缀
相等前后缀是说,一个字符串的前缀和后缀是相等的
举例:
aabaabaa
相等前后缀为:aa
3.next数组
next数组是kmp算法的一个核心,该数组记录的目标字符串中,从第0个字符开始,到第i个字符的相等前后缀长度
a a b a a f
next=[0,1,0,1,2,0]
kmp算法
匹配字符串除了使用kmp算法,还可以使用暴力匹配,暴力匹配需要我们反复移动主串上的指针,而kmp算法的精髓就在于主串上的指针只会向后移动,不会向前移动
function kmp_search(str, patt) {
// 先根据目标字符串构建next数组
const next = build_next(patt)
let i = 0, //主串指针
j = 0 //子串指针
while (i < str.length) {
// 如果主串[i]和目标串[j]相等,则将i和j向后移动,继续进行匹配
if (str[i] == patt[j]) {
i++
j++
// 如果不相等,且已经匹配目标串的一部分,这时候就需要使用到next数组,下面会进行详解
} else if (j > 0) {
j = next[j - 1]
// 如果不相等,且匹配过的字符串还没有和目标串相同的部分,就直接往后继续移动主串就好了
} else {
i++
}
if (j == patt.length) {
return i - j
}
}
}
next数组使用
从上面例子我们看到,当目标字符串和主串已经有一部分匹配,但是第i个字符串不匹配了,这时候不需要将目标字符串上指针指回初始位置,而是借助了next数组,其实这不难理解
从图中我们看出,主串的b和目标字符串的f不匹配,但是f之前的字符都匹配上了,这时候我们就要充分利用这些匹配过的子串,我们不难发现,既然之前已经匹配上了,那么我们只要找到目标字符串的最长相等前后缀,可以直接将指针从后缀移动到前缀,在这个例子中也就是直接从f移动到b,而我们如何准确移动到b的位置呢,这就要借助next数组
next数组构建
思路:
1.我们构建两个指针,一个用于到当前字符串位置最长公共前后缀长度,一个用于遍历整体串
2.整体思路就是,从第一个字符串开始不断往后添加,注意next数组记录的是从第1个字符到当前字符公共前后缀的长度
3.新增一个字符无非就两种情况,一种和当前公共前后缀的最后一个字符串相等,这时候我们只需要向后继续遍历即可,另外一种情况,就是不相等,当当前子串和当前公共前后缀不相等时,我们就需要去缩短公共前后缀的长度,依次进行匹配,直至匹配上或者到0为止
代码实现:
function build_next(str) {
// 寻找子串中最长公共前后缀
const next = [0]
let prefix = 0, //最长公共前后缀长度
i = 1 //因为1个字符串不存在公共前后缀,因此需要从第2个字符既i=1开始
while (i < str.length) {
if (str[prefix] == str[i]) {
prefix++
next.push(prefix)
i++
} else {
if (prefix == 0) {
next.push(0)
i++
} else {
console.log(str[i], '=>', prefix)
prefix = next[prefix - 1]
}
}
}
return next
}