kmp算法
首先我们先来介绍一下什么是kmp算法,我们先来看一下概念上的东西。KMP算法,全称Knuth-Morris-Pratt算法,是一种改进的字符串匹配算法。它是由Donald Knuth、Vaughan Pratt和James H. Morris在1977年共同提出的。KMP算法的主要优点是在匹配过程中,不会回溯文本指针i,不会重复遍历已经比较过的字符,只通过修改模式指针j来进行字符串的匹配。
KMP算法的核心思想是利用已经部分匹配的有效信息,使得模式在文本中尽可能少的移动,提高匹配效率。这种技术被称为"部分匹配"或"失败函数"。
KMP算法主要包括两个步骤:
- 计算部分匹配表(也称为失败函数) :部分匹配表是预处理模式字符串的过程,用于记录模式字符串的自我匹配信息。表中的每个值都表示当模式字符串的某个字符与文本字符串的字符不匹配时,模式字符串应该从哪个位置开始比较。
- 字符串匹配 如果让我们实现一个字符串比对,比如两个字符串str1 = "BBC ABCDAB ABCDABCDABDE":在这个过程中,主要是通过比较文本字符串和模式字符串,然后根据部分匹配表移动模式字符串的位置,直到找到匹配的字符串或者遍历完整个文本字符串。
KMP算法的时间复杂度是O(m+n),其中m是文本字符串的长度,n是模式字符串的长度。这使得KMP算法在长字符串匹配中非常高效。
总的来说,KMP算法是一种高效的字符串匹配算法,它通过预处理模式字符串,避免了不必要的字符比较,从而提高了匹配效率。
kmp算法的传统实现计算部分匹配表
我第一次接触kmp算法是在我们公司内部的一个交流会上,分享者使用了阮一峰的思路来举例了kmp算法。首先上面也介绍了kmp算法是分成两个步骤,1. 计算部分匹配表(也称为失败函数) 2. 字符串匹配,首先我们先来看一下计算部分分配表。先看一下传统的方案(阮一峰方案解法)。他先讲了两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。这里的两个概念是什么意思呢,可以用一幅图来表明:

所谓的匹配值就是"前缀"和"后缀"的最长的共有元素的长度。以ABCDABD为例,
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
所以我们可以得出结论即ABCDABD的匹配表为[0,0,0,0,1,2,0]。ts的coding方案
ts
function computePrefixTable(pattern: string): number[] {
const prefixTable = [0];
let len = 0;
let i = 1;
while (i < pattern.length) {
if (pattern[i] === pattern[len]) {
len++;
prefixTable[i] = len;
i++;
} else {
if (len !== 0) {
len = prefixTable[len - 1];
} else {
prefixTable[i] = 0;
i++;
}
}
}
return prefixTable;
}
console.log(computePrefixTable("ABCDABD")); // Output: [0, 0, 0, 0, 1, 2, 0]
kmp优化方案计算部分匹配表
首先其实经过我们的观察所谓的共有元素有一个特点,他是从第一个元素到第一次重复元素的字符串的遍历,我们可以重新观察一遍字符串ABCDABD,它对应的匹配表[0,0,0,0,1,2,0]。通过这个规律我们可以这样做首先我们依旧是遍历整个字符串,创建一个长度为字符串长度的数组next,数组中每一个元素都赋值0,创建两个指针,即i和j如图

我们先去比对索引为str[i]是否和str[j]相同如果相同则让j++,我们可以看到在遍历的过程中i=4的时候str[i]即str[4]是等于str[j]即str[0]的。我们让next[i]=j,当我们做完下面的操作之后重新看整个过程如图:

ok,下一步,我们在比对str[i]是否和str[j]相同如果相同之前先做一件事情,我们先判断一下j是否大于0,如果j大于0那我们就在做一个判断,str[i]是否和str[j]不相同,如果不相同那么就让j=next[j-1],如果相同那我们就继续让j++,然后赋值给next[i] = j,如图


讲了这么多让我们来写一下他的coding实现
ts
function createNext(pattern: string): number[] {
let next = new Array(pattern.length).fill(0);
let j = 0;
for (let i = 1; i < pattern.length; i++) {
while (j > 0 && pattern[i] !== pattern[j]) {
j = next[j - 1];
}
if (pattern[i] === pattern[j]) {
j++;
}
next[i] = j;
}
return next;
}
kmp算法字符串匹配
如果让我们实现一个字符串比对,比如两个字符串str2="ABCDABD",str1="BBCABCDABABCDABCDABDE",我想做一件事就是str1中是否包含str2,最简单的方案就是我有一个index指针,他的起始位置是0,然后截取str1中str2的长度的字符串和str2进行比较,如果相等就返回index,及BBCABCD和ABCDABD作比较,如果不相等就让index索引往后移动一位。在这里我们会发现一些事情比如当我们的索引值到3的时候,及ABCDABA和ABCDABD作比较之后,我们的索引再往后移动其实在我们比对完成之后就会发现,如果他想出现ABCDABD的话最起码要移动到ABCDABA 这个位置因为别管我是移动到ABCDABA还是其他的位置都不可能出现ABCDABD的情况。
所以这里我们可以跳着移动,这时候我们就需要引入一个公式了移动位数 = 已匹配的字符数 - 对应的部分匹配值。这里阮一峰的文章有更详细的图进行演示。这里我们知道需要跳跃的逻辑之后可以直接写coding了。
ini
ts
function KMP(text: string, pattern: string): number {
const prefixTable = createNext(pattern);
let i = 0;
let j = 0;
while (i < text.length) {
if (pattern[j] === text[i]) {
if (j === pattern.length - 1) {
return i - j;
}
i++;
j++;
} else {
if (j !== 0) {
j = prefixTable[j - 1];
} else {
i++;
}
}
}
return -1;
}
总结
kmp相对于传统的暴力算法有更高的时间复杂度,因为他是跳跃着比较字符串的,时间复杂度比n要低,具体是要看匹配表,本文主要是用了其他的思想来解决生成匹配表的方案。
参考