[数据结构与算法] 字符串的匹配算法(KMP算法)
标签: 实现strStr
KMP算法
上一篇文章概述了一下BF算法以及缺点,这篇文章来说明一下KMP算法,对BF算法的优化。
如果你还不知道BF算法是什么: 点击我 先了解匹配字符串的暴力匹配法,然后再看KMP,感觉会更清晰一点。
KMP算法
这个算法,是对BF算法的一个优化,为什么说优化呢,KMP算法当两个字符不相等的时候,i不需要回溯,只需要j回溯,j回溯到什么位置呢?先不看这个问题,先看kmp算法的例子。
假设给定的str是a b a b c a a b c a c b c b
给定的pattern是a b c a c
推导过程手写了一份:
第一步:
i
指向当前str
的索引,j
指向pattern
的索引,从0开始相等,然后i++,j++
。当i==2 j==2
遇到不相等的情况:
第二步:
第三步:
为什么会是这样:
总结,实际上最大前后缀,咱们可以细细想一下上面的例子。
此时str 是a b c x y z a b c x y z a b c d c d
pattern是 a b c x y z a b c d
比较到最后一个字符 x 和 d是不等的。这时候说明,pattern
中字符d
以前 的字符已经完全和str中的 x
字符以前的字符完全匹配。这时候我们只需要关注pattern中j的位置的变化,也就是求d
字符的索引j的下一个位置,也就是next表,也即使说这个d
字符对应的j要移动到的位置的关系表。 d
为j
的索引,如果和对应 的i
值不相等,那么应该跳回到j
以前的字符的最大公共前缀,也就是next表中的 next[j]的值。
BF算法是这样:
const strStr = (str, pattern) => {
let i = 0, j = 0
while (i < str.length && j < pattern.length ) {
if (str[i] == pattern[j]) {
i++
j++
} else {
i = i - j + 1 // 回溯到当前比较的起点的下一个位置
j = 0
}
}
if (j == pattern.length) return i - j
return -1
}
利用KMP算法的话,i 不需要变化,j回溯,代码会是下面这样:
const strStr = (str, pattern) => {
let i = 0, j = 0
while (i < str.length && j < pattern.length ) {
if (str[i] == pattern[j] || j == -1) {
// 为什么j==-1为判定条件之一,先忽略,往后面看
i++
j++
} else {
// 优化之前
// i = i - j + 1 // 回溯到当前比较的起点的下一个位置
// j = 0
// 优化之后
j = next[j] // next表存储着这个位置的下个回溯位置
}
}
if (j == pattern.length) return i - j
return -1
}
那么问题来了,怎么求这个next表,也就是对应j位置的下一个回溯位置,代码如下:
function getPrefixTable (pattern) {
let prefix = []
prefix[0] = 0
let len = 0; // 最大前后缀的相等的数目 【包含当前字母的字符串的最大相等前后缀的长度】
let i = 1; // i 从pattern的第二个字母开始比较,为什么,因为第一个字母比较不相等j不需要回溯,i和j都往后移动一个即可。
while (i < pattern.length) {
/**
* https://www.youtube.com/watch?v=3IFxpozBs2I 7:59s
* ABABCA [这个字符] 的prefix值是啥,这个字符前面的最大重复前后缀为1 len = 1
* 如果这个字符 等于 pattern[1] 那么必然有两个相同的前后缀
* 所以每个位置的相同的前后缀是和前面一个字符的相同最大前后缀长度相关联的
*
*/
if (pattern[i] == pattern[len]) {
len++
prefix[i] = len
i++;
} else {
// len 可能等于 -1
// WRONG
// len = prefix[len - 1]
if (len > 0) {
len = prefix[len - 1]
} else {
// let t = 'ABABCABAA' i = 1 ; len = 0 程序进入的时候就死循环
// 所以
prefix[i] = len
i++;
// prefix[i] = 0 都可以
}
}
}
console.log(prefix)
return moveTable(prefix)
}
// j对应的next表的值,求的是 pattern[0] 到 pattern[j - 1] 的最大公共字符串。
// 没有调用moveTable之前,是包含了自己的字符
function moveTable(prefix) {
let j = prefix.length;
for (let i = j - 1; i > 0; i--) {
prefix[i] = prefix[i - 1]
}
prefix[0] = -1
return prefix
}
let next = getPrefixTable(pattern)
好的我知道你肯定一脸懵逼,这个推导过程while循环内的,if语句判定相等的过程是容易理解的,不相等的情况有点迷糊,直接看这个油管up主黄浩杰的讲解 ,楼主也是看了很多视频和文章,感觉就这个最容易理解点。
资料:
王卓老师的讲解bp算法是好理解,KMP的算法原理也好理解【很关键,知道怎么跳过无意义的比较】,但是next表也就是j回溯位置的数组求解就有点懵,而且王老师计算是索引1开始,数组一般是0开始,需要转换一下。
建议先看王老师的原理,然后看油管黄浩杰的 next表的求解原理。