KMP算法详解(参考代码随想录)
KMP的经典思想:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
前缀表
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
next数组就是一个前缀表(prefix table):记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
最长公共(相等)前后缀
前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
因为前缀表要求的就是相同前后缀的长度。
为什么使用前缀表
因为当模式串与文本串匹配失败时,当前匹配失败的字符串不包含当前字符的后缀与文本是匹配的,因此通过找到与后缀相等的前缀就可以跳过对前缀的匹配判断。
如何计算前缀表
模式串:aabaaf
a,最长相等前后缀长度:0
aa,最长相等前后缀长度:1,最长相等前后缀:a
aab,最长相等前后缀长度:0
aaba,最长相等前后缀长度:1,最长相等前后缀:a
aabaa,最长相等前后缀长度:2,最长相等前后缀:aa
aabaaf,最长相等前后缀长度:0
前缀表与next数组
next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)
使用next数组来匹配
时间复杂度分析
匹配过程O(n)
,生成next数组O(m)
总时间复杂度O(n+m)
构造next数组
构造next数组==计算前缀表,主要有以下三步:
- 初始化
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
1. 初始化
两个指针:i
指向后缀末尾位置,j
指向前缀末尾位置
next数组初始化赋值:
int j=0;
next[0]=j;
其中,j
初始化为0,i
初始化为1。
由于j
指向前缀末尾位置,因此j+1
是当前前缀的长度,也就是前后缀相等的长度。
其中,next[i]存放的是i
之前(包括i
)最长相等前后缀长度。
1. 为什么i
初始化为1?
原因:后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串,i=0的位置是该字符串的第一个字符,因此指向后缀末尾位置的i
不能从0开始。
2. 为什么j+1
是前后缀相等的长度?
原因:当j
指向的元素与i
指向的元素匹配时,在j
指向的元素之前的每个元素(包括j
所指的元素)都与i
指向的元素之前的每个元素(包括i
所指的元素)匹配,即j
所表示前缀与i
所表示的后缀匹配。因此,j
的位置能代表前后缀相等的长度。由于j
是下标,且从0开始,因此长度需要加一,即j+1
。
2. 处理前后缀相同的情况
当s[i]==s[j]
时,说明找到了相同的前后缀,将当前相等前后缀的长度j++
之后的j
,即j+1
,赋值给next[i]
数组。i
和j
分别往后移动一位,比较下一个元素,j
在j++
中移动,i
在循环条件中移动。
if (s[i] == s[j]) {
j++;
}
next[i] = j;
3. 处理前后缀不相同的情况
当s[i]!=s[j]
时,说明当前前后缀的末尾不同,因此j
向前回退j=next[j-1];
1. 为什么只比较前缀末尾j
和后缀末尾i
单个元素就能判断前后缀是否相同?
原因:
-
i
在扫描过程中相当于隐性地存下了后缀(从下标1位置到下标i
位置都是当前所指字符串的后缀)
-
j
能够向后移动,说明j
的前面与i
的前面一定是匹配的,且匹配的元素个数是j+1
个,即i
前的j+1
个元素匹配。
- 因此,表面上只比较了一个元素,实际上在移动过程中,前面的元素都被比较过。
2. 回退的逻辑?
- 回退的逻辑本质上与模式串和文本串匹配逻辑相同
- 回退是在寻找
j
所表示的字符串中的最长相等前后缀,当s[i]!=s[j]
时,0~j-1
所表示的j
个元素构成的字符串1与i-j~i-1
所表示的j
个元素构成的字符串2是匹配的。因此,我们只需要找到字符串1中的最长相等前后缀,即next[j-1],就能跳过前缀的重新匹配(因为在最长相等前后缀中,前缀与后缀是相同的,i
前部分元素与后缀相同,因此前缀和i
前部分元素相同,所以不用再匹配)
前缀表的代码实现
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
使用next数组来做匹配
在文本串s中寻找是否出现过模式串
定义下标j
指向模式串起始位置,i
指向文本串起始位置,其中,j=0;i=0;
。
如果s[i]与t[j]不相同,j
从next数组中寻找下一个匹配位置
while(j > 0 && s[i] != t[j]) {
j = next[j-1];
}
如果s[i]与t[j]相同,那么i
和j
同时向后移动
if (s[i] == t[j]) {
j++; // i的增加在for循环里
}
判断在文本串s里出现了模式串t:
如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。
如果要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。
if (j == t.size()) {
return (i - t.size() + 1);
}
前缀表(不减一)的实现
class Solution {
public:
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
f (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};