🍁什么是KMP算法?
- KMP算法是一种改进的 字符串匹配算法,由 D.E.Knuth,J.H.Morris 和V.R.Pratt 提出的,因此人们称它为 克努特—莫里斯—普拉特 操作(简称 KMP 算法)。
- KMP 算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
- 具体实现就是通过一个
next()
数组实现,数组本身包含了模式串的局部匹配信息。KMP 算法的时间复杂度 O ( m + n ) O(m+n) O(m+n)。
🍁什么是 next() 数组 和 前缀表?
next
数组就是一个前缀表(prefix table)!
前缀表有什么作用呢
前缀表是用来回退的,它记录了 模式串 与 主串(文本串) 不匹配的时候,模式串 应该从哪里开始重新匹配。
我们来举一个例子:
- 要在文本串:
aabaabaafa
中查找是否出现过一个模式串:aabaaf
。
如动画所示:
最长公共前后缀
- 字符串的前缀是:指不包含最后一个字符的所有 以第一个字符开头的连续子串。
- 后缀:是指不包含第一个字符的所有 以最后一个字符结尾的连续子串。
正确理解什么是前缀什么是后缀很重要!
可以理解为:最长相等前后缀
如何计算前缀表
注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
- 长度为前1个字符的子串
a
,最长相同前后缀的长度为0
。
- 长度为前2个字符的子串
aa
,最长相同前后缀的长度为1
。
3. 长度为前3个字符的子串 aab
,最长相同前后缀的长度为 0
。
- 以此类推: 长度为前4个字符的子串
aaba
,最长相同前后缀的长度为1
。 - 长度为前5个字符的子串
aabaa
,最长相同前后缀的长度为2
。 - 长度为前6个字符的子串
aabaaf
,最长相同前后缀的长度为0
。
🚀 构造next数组
构造 next
数组其实就是计算 模式串 s
:
- 定义两个指针
i
和j
,j
指向前缀末尾位置,i
指向后缀末尾位置。 next[i]
表示i
(包括i
)之前最长相等的前后缀长度(其实就是j
)。
next
数组就可以是前缀表,但是很多实现都是把 前缀表统一减一(右移一位,初始位置为 -1
)之后作为 next
数组。前缀表的构造过程,主要有如下三步:
- 初始化:
j
初始化为 -1;
- 处理前后缀不相同的情况
- 因为
j
初始化为-1
,那么i
就从1
开始,进行s[i]
与s[j+1]
的比较; - 如果
s[i]
与s[j+1]
不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。next[j]
就是记录着j
(包括j
)之前的子串的相同前后缀的长度;- 那么
s[i]
与s[j+1]
不相同,就要找j+1
前一个元素在next
数组里的值(就是next[j]
)。
- 因为
- 处理前后缀相同的情况
- 如果
s[i]
与s[j + 1]
相同,那么就同时向后移动i
和j
说明找到了相同的前后缀,同时还要将j
(前缀的长度)赋给next[i]
, 因为next[i]
要记录相同前后缀的长度。
- 如果
构造 next
数组的逻辑流程动画如下:
构造 next
数组的函数如下:(C++)
void getNext(int* next, const string& s){
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
🚀 使用next数组来做匹配
在文本串 s
里找是否出现过模式串 t
。
- 定义两个下标
j
指向模式串起始位置,i
指向文本串起始位置; j
初始值依然为-1
;- 接下来就是
s[i]
与t[j + 1]
(因为j
从-1
开始的)进行比较:- 如果
s[i]
与t[j + 1]
不相同,j
就要从next
数组里寻找下一个匹配的位置; - 如果
s[i]
与t[j + 1]
相同,那么i
和j
同时向后移动。
- 如果
- 如果
j
指向了模式串t
的末尾,那么就说明模式串t
完全匹配文本串s
里的某个子串了。
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = -1; // // 因为next数组里记录的起始位置为-1
for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始
while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t
return (i - needle.size() + 1);
}
}
return -1;
}
匹配过程如下:
时间复杂度:
O
(
n
+
m
)
O(n + m)
O(n+m)。
空间复杂度:
O
(
m
)
O(m)
O(m), 只需要保存字符串 needle
的前缀表。
放弃一件事很容易,每天能坚持一件事一定很酷,一起每日一题吧!
关注我LeetCode主页 / CSDN—力扣专栏,每日更新!
注:仅供学习参考,如有不足,欢迎指正!