KMP 算法
KMP 算法解决的是字符串匹配的问题,其经典思想是:当出现的字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
前缀表
next 数组就是一个前缀表。前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
前缀表是记录下标 i 之前(包括 i)的字符串中,有多大长度的相同前缀后缀。
最长相等前后缀
字符串的前缀是包含首字母,不包含尾字母的所有的连续字符串
后缀是包含尾字母,不包含首字母的所有字符串
前缀表要求的是相同前后缀的长度
为什么一定要用前缀表
当来当下标 5 是发现模式串与当前文本串 aabaabaafa 不匹配,下标 5 之前这部分的字符串(即字符串 aabaa)的最长相等的前缀和后缀字符串是子字符串 aa,匹配失败的位置是后缀子串的后面,因此我们需要找到与之相同的前缀后面进行重新匹配,即回到下标为 2 的位置。
前缀表的计算
对于字符串 aabaaf 其前缀表为
字符子串 | 最长相等前后缀长度 |
---|---|
a | 0 |
aa | 1 |
aab | 0 |
aaba | 1 |
aabaa | 2 |
aabaaf | 0 |
当找到不匹配的位置时,我们需要看其前一个字符的前缀表的数值是多少,然后回退到对应的位置。
前缀表与 next 数组
next 数组既可以是前缀表,也可以是前缀表统一减 1 或者右移一位。
Leetcode 28-实现 strStr()
题目描述:
https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/description/
解题思路
构造 next 数组
构造 next 数组就是计算模式串 needle 的前缀表的过程,主要可以分为三步:
- 初始化
定义两个指针 i 和 j,j 指向前缀末尾位置,i 指向后缀末尾位置。
同时对 next 数组进行初始化赋值:
int j = 0;
next[0] = 0;
这里 j 初始化为 0 是因为前缀从下标为 0 的位置(首字母)开始,next[0]=0 是因为只有首字母的子字符串的最长相等的前后缀长度为 0
i 因为是后缀,所以初始化时为 1
- 处理前后缀不相同的情况
for (int i = 1; i < s.size(); i++) {while (s[j] != s[i] && j > 0) {//j大于0,因为下方操作中有将j-1作为数组下标的操作j = next[j - 1];}
- 处理前后缀相同的情况
if (s[j] == s[i]) {j++;
}
构造 next 数组的完整代码是
//i指向后缀末尾位置,j指向前缀末尾位置
void getNext(int* next, const string& s) {int j = 0;next[0] = 0;for (int i = 1; i < s.size(); i++) {while (s[j] != s[i] && j > 0 ) {//j大于0,因为下方操作中有将j-1作为数组下标的操作j = next[j - 1];}if (s[j] == s[i]) {j++;}next[i] = j;}
}
使用 next 数组进行匹配
在此基础上,我们可以使用 next 数组完成匹配,找出模式串是否再文本串中出现过
定义两个下标 i 和 j,其中 j 指向模式串的起始位置,i 指向文本串的起始位置
int strStr(string haystack, string needle) {//j指向模式串的起始位置,i指向文本串的起始位置if (needle.size() == 0) {return 0;}vector<int>next(needle.size());getNext(next, needle);int j = 0;for (int i = 0; i < haystack.size(); i++) {while (haystack[i] != needle[j] && j > 0) {j = next[j - 1];}if (haystack[i] == needle[j]) {j++;}if (j == needle.size()) {return(i - needle.size() + 1);}}return -1;
}
完整代码为:
class Solution {
public://i指向后缀末尾位置,j指向前缀末尾位置void getNext(vector<int>& next, const string& s) {int j = 0;next[0] = 0;for (int i = 1; i < s.size(); i++) {while (s[j] != s[i] && j > 0) {//j大于0,因为下方操作中有将j-1作为数组下标的操作j = next[j - 1];}if (s[j] == s[i]) {j++;}next[i] = j;}}int strStr(string haystack, string needle) {//j指向模式串的起始位置,i指向文本串的起始位置if (needle.size() == 0) {return 0;}vector<int>next(needle.size());getNext(next, needle);int j = 0;for (int i = 0; i < haystack.size(); i++) {while (haystack[i] != needle[j] && j > 0) {j = next[j - 1];}if (haystack[i] == needle[j]) {j++;}if (j == needle.size()) {return(i - needle.size() + 1);}}return -1;}
};
要注意 class 中默认的函数和参数权限是 private,如果函数要在 main 中使用需要声明 public(来自一个在 leetcode 里面报错才发现这个问题的小白 😔)
时间复杂度分析
若文本串的长度为 n,模式串的长度为 m,使用 next 数组匹配的过程时间复杂度是 O ( n ) O(n) O(n),生成 next 数组的时间复杂度为 O ( m ) O(m) O(m),因此使用 KMP 算法解决
Leetcode 459-重复的子字符串
题目描述:
https://leetcode.cn/problems/repeated-substring-pattern/description/
解题思路
移动匹配
移动匹配的思路是,如果字符串内部是由重复的子串构成的,那么 s+s 组成的字符串在刨除首字符和尾字符后剩余的字符一定还能组合一个 s
class Solution {
public:bool repeatedSubstringPattern(string s) {string t = s + s;t.erase(t.begin());t.erase(t.end()-1);if (t.find(s) != std::string::npos)return true;//判断s+s字符串在去掉首尾字符之后是否还包含sreturn false;}
};
KMP 算法
找到最小重复子串:字符串中最长前后缀不包含的字串就是该字符串的最小重复子串
由此可知,数组长度减去最长相同前后缀的长度就是重复子串的长度,如果字符串的长度可以整除重复周期,则说明该字符串由一个子串重复构成
代码如下,使用上道例题中实现的 getNext 代码计算 next 数组的值,然后在 repeatedSubstringPattern 函数中判断数组长度是否能整除重复子串的长度
class Solution {
public:void getNext(int* next, string& s) {int j = 0;next[0] = 0;for (int i = 1; i < s.size(); i++) {while ((j > 0) && (s[j] != s[i])) {j = next[j - 1];}if (s[j] == s[i]) {j++;}next[i] = j;}}bool repeatedSubstringPattern(string s) {if (s.size() == 0)return false;vector<int>next(s.size());getNext(&next[0], s);int len = s.size();if (next[len - 1] != 0 && len % (len - (next[len - 1])) == 0) {return true;}return false;}
};
字符串总结
vector和 string 的区别
vector和 string 在基本操作上没有区别,但是 string 提供更多的字符串处理的相关接口,例如 string 重载了 +。