字符串匹配—KMP算法
KMP算法有什么用
KMP主要应用在字符串匹配上。KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。KMP算法的关键在于利用部分匹配信息来避免重复的字符比较,从而在O(n)时间复杂度内完成匹配操作(n是主串的长度)。
例题 1
28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
示例 1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
KMP算法核心
1.最长相等前后缀
前缀:不包含最后一个字符的所有以第一个字符开头的连续子串;
后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串。
举例说明:“abab”:前缀:a
,ab
,aba
;后缀:b
,ab
,bab
;最大的相同前缀和后缀是 “ab”,长度是 2。
举例说明:以字符串aabaaf为例
a -> 0; a a -> 1; a a b -> 0; a a b a -> 1; a a b a a -> 2; a a b a a f -> 0;
//这里最长的相等前后缀就是找子串里前缀字符和后缀字符相等的对数,这里显然最大的是2
2.算法实现关键
以主串为“aabaabaafa”,模式串“aabaaf”为例,模式串与主串一对一匹配时,在模式串下标为5的字符f
处不一致,于是要回退重新去匹配,那怎么回退?
查询f
前一个字符a
(模式串下标为4)对应的最长相等前后缀为2
,于是就回退到模式串下标为2的b
处继续开始遍历匹配(模式串aabaaf
中的b
和主串aabaabaafa
中的第二个b
,也就是模式串f
之前对应的主串的b
)。所以这个算法核心就是构建最长相等前后缀表,也就是所谓的next[ ]。(看不懂的小伙伴可以看代码随想录讲解)
a a b a a b a a f a
a a b a a f
0 1 2 3 4 5 //模式串下标序号
0 1 0 1 2 0 //最长相等前后缀 (next[])
代码实现
首先是实现next数组,获取模式串的最长相等前后缀表,具体代码实现如下:
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;
}
}
完整的解题代码(LeetCode题目)
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;
}
vector<int> next(needle.size());
getNext(&next[0], 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;
}
};
KMP算法应用:重复的子字符串
459. 重复的子字符串
给定一个非空的字符串 s
,检查是否可以通过由它的一个子串重复多次构成。
示例 1:
输入: s = "abab"
输出: true
解释: 可由子串 "ab" 重复两次构成。
示例 2:
输入: s = "aba"
输出: false
示例 3:
输入: s = "abcabcabcabc"
输出: true
解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)
题解
之前我们已经知道了next数组可以获取字符串的最长相等前后缀,那么这能否来判断子串重复呢,显然可以的。
数组长度为:len
;next[len - 1]>0
len - next[len - 1]
是最长相等前后缀不包含的子串的长度;
如果len % (len - next[len - 1]) == 0
,则说明数组的长度正好可以被最长相等前后缀不包含的子串的长度 整除 ,说明该字符串有重复的子字符串。
举例说明:
字符串: a s d f a s d f a s d f
next数组数值: 0 0 0 0 1 2 3 4 5 6 7 8
next[len - 1] = 8
,8就是此时字符串asdfasdfasdf的最长相同前后缀的长度。
(len - (next[len - 1] ))
也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 为最长相同前后缀不包含的子串长度。
4可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。
代码实现
class Solution {
public:
void getNext (int* next, const string& s){
next[0] = 0;
int j = 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;
}
}
bool repeatedSubstringPattern (string s) {
if (s.size() == 0) {
return false;
}
int next[s.size()];
getNext(next, s);
int len = s.size();
if (next[len - 1]>0 && len % (len - (next[len - 1] )) == 0) {
return true;
}
return false;
}
};
总结
这种类型题目都可以采用暴力解法,套两层for循环也可以实现,但是时间复杂度为:O(n*m),采用KMP算法之后,时间复杂度为O(n + m)。所以掌握KMP算法还是有意义的。