(本文是个人理解+实现,留作备忘,如有问题欢迎讨论)
较之暴力匹配,kmp的优点在于对源字符串(待匹配的母串)遍历时省略掉了部分循环,在某些情景下可以带来可观的性能提升。而部分匹配表解法的核心是通过对比模式串
的前缀和后缀(定义不再赘述)得到一个部分匹配表
数组,从而对循环进行优化。
首先,要了解两个概念:“前缀"和"后缀”。
前缀指除了最后一个字符以外,一个字符串的全部头部组合;
后缀指除了第一个字符以外,一个字符串的全部尾部组合
例如字符串elecat
,前缀有e,el,ele,elec,eleca
,后缀有t,at,cat,ecat,lecat
先看一下暴力匹配
function bruteForce(source, pattern) {
let source_len = source.length,
pattern_len = pattern.length,
result = [];
for (let i = 0; i < source_len; i++) {
let index = 0;
while (
(source.charCodeAt(i + index) === pattern.charCodeAt(index)) && (source_len - i > pattern_len)
) {
index++;
if (index === pattern_len) result.push(i);
}
}
return result.length === 0 ? -1 : result
}
不论上一次匹配的结果如何,下一次的外部循环i
总是+1
接着先上获取部分匹配表
的代码,再看其作用
function getPartMatch(str) {
let result = [0],
len = str.length;
for (let i = 1; i < len; i++) {
let child_str = str.substring(0, i + 1);
for (let j = 0, j_len = child_str.length; j < j_len; j++) {
let prefix = child_str.substring(0, j),
suffix = child_str.substring(j_len - j, j_len);
if (prefix === suffix && prefix !== '') {
result[i] = prefix.length;
}
if (!result[i]) result[i] = 0
}
}
return result
}
getPartMatch('ABCDABD')//[0, 0, 0, 0, 1, 2, 0]
部分匹配表的作用:当模式串与源字符串分别匹配前1~n(n是源字符串的长度)位时,部分匹配表的[n-1]位记录着下一次匹配已知的有效匹配位数
这句话可能有点难理解,还是用代码说话
const sourceStr = 'BBC ABCDABABCDAB ABCDABCDABDE';//源字符串
const patternStr = 'ABCDABD';//模式串
//刚才已经得到模式串的部分匹配表是
// [0, 0, 0, 0, 1, 2, 0]
//第1次匹配时(懒得画图了):
B|B|C| |A|B|C|D|A|B|A|B|C|D|A|B| |A|B|C|D|A|B|C|D|A|B|D|E
A|B|C|D|A|B|D
//没有1位匹配,无需查看部分匹配表
//后移1位,进入第2次匹配
B|B|C| |A|B|C|D|A|B|A|B|C|D|A|B| |A|B|C|D|A|B|C|D|A|B|D|E
|A|B|C|D|A|B|D
//同上,继续向下匹配
//第3次匹配
B|B|C| |A|B|C|D|A|B|A|B|C|D|A|B| |A|B|C|D|A|B|C|D|A|B|D|E
| |A|B|C|D|A|B|D
//第4次匹配
B|B|C| |A|B|C|D|A|B|A|B|C|D|A|B| |A|B|C|D|A|B|C|D|A|B|D|E
| | |A|B|C|D|A|B|D
//第5次匹配
B|B|C| |A|B|C|D|A|B|A|B|C|D|A|B| |A|B|C|D|A|B|C|D|A|B|D|E
| | | |A|B|C|D|A|B|D
注意这时,模式串的前6位(也就是有效匹配内容)全部匹配上了,查阅部分匹配表的[6-1]
位,值是2
,表明虽然没有完全匹配,但是我们可以知道:除了有效匹配内容的最后2位,其他的部分我们可以直接跳过,不进行循环。
所以我们下一次循环时,将模式串的第1位与刚才有效匹配内容的倒数第2位对齐即可
//第6次匹配
B|B|C| |A|B|C|D|A|B|A|B|C|D|A|B| |A|B|C|D|A|B|C|D|A|B|D|E
| | | | | | | |A|B|C|D|A|B|D
在这次匹配中模式串的前2位匹配上,部分匹配表的[2-1]
位值是0
,即,此次的2
位有效内容下次循环均可跳过。
//第7次匹配
B|B|C| |A|B|C|D|A|B|A|B|C|D|A|B| |A|B|C|D|A|B|C|D|A|B|D|E
| | | | | | | | | |A|B|C|D|A|B|D
···
如此往复。
知道了部分匹配表的核心理念,在暴力匹配的基础上加以修改即可
function kmpPartMatch(source, pattern) {
let partMatch = getPartMatch(pattern),
source_len = source.length,
pattern_len = pattern.length,
result = [];//使用数组缓存匹配的所有结果,也可修改为类似indexOf只返回第一个匹配处的下标
// console.log(partMatch);
// console.log(source);
for (let i = 0; i < source_len - pattern_len; i++) {
let index = 0;//模式串游标
//console.log(`检索到第${i}个字符`);
while (
(source.charCodeAt(i + index) === pattern.charCodeAt(index)) && (source_len - i > pattern_len)
//源字符串与模式串对应下标的字符相等&&源字符串剩余的长度还满足一个模式串的要求,也规避掉了一些无效循环
) {
index++;//游标后移
if (index === pattern_len) result.push(i);//游标移动的长度=模式串的长度,说明满足匹配,记录结果
}
if (index > 0) {//index大于0说明游标移动了,即模式串与源字符串是有匹配的部分
// console.log(`匹配上的长度:${index}`);
let step = index - partMatch[index - 1] - 1;//考虑到for循环体的自增操作,最后再多减一
// console.log(`步长:${step}`);
// console.log(`将要去第${i + step}个字符`)
i += step;
} else {
// console.log('匹配失败');
}
// console.log('--------------------');
}
return result.length === 0 ? -1 : result
}
console
语句和注释比较多,有不明白的地方可以将console
语句打开看一下log,方便理解