k+1
个元素匹配失败时,我们知道前面k
个字符都是匹配上了的,即P[0:k] = T[q:q+k]
(左闭右开区间)。这里面是有信息可以使用的,我们可以尝试找到P[0:k]
中最长的前缀0 ~ j
,满足与后缀k-j ~ k
相同,那就可以只回退模式串指针来比较P[j+1]
与T[q+k]
,如果相等比较继续进行,如果失败继续回退模式串。left = 0, right = 1
。left 代表相同前缀的最后位置,right 代表后缀的最后位置,同时 left 也是相等前后缀的长度。p[left] != p[right]
,那就需要回退 left,这与字符串匹配失败是类似的,我们要尽量保留已经确定相等的前后缀(利用已经匹配的信息),所以我们使用相同的方式回退即可(循环过程的不变量);如果二者相等,left 向后更新一个位置。class Solution{
public:
void getNext(int* next, string s){
int left = 0;
next[0] = left;
for(int right = 1; right < s.size(); right++){
while(left > 0 && s[left] != s[right]){
left = next[left - 1];
}
if(s[left] == s[right]){
left++;
}
next[right] = left;
}
}
int strStr(string haystack, string needle){
int i = 0; // 文本串指针
int j = 0; // 模式串指针
int next[needle.size()];
getNext(next, needle);
for(; 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;
}
};
有一些实现中,next 数组取值为前缀表整体减1,其实和这种原封不动使用前缀表的实现相比,原理是一样的,只是 KMP 算法不同的实现而已。但要注意回退时的下标与遍历时的下标,整体减1的实现中,在比较时模式串指针应该在 j+1 的位置,回退时则不需要进行 j-1 的操作,j = next[j]
。
class Solution{
public:
void getNext(int* next, string s){
int left = -1;
next[0] = left;
for(int right = 1; right < s.size(); right++){
while(left > -1 && s[left + 1] != s[right]){
left = next[left];
}
if(s[left + 1] == s[right]){
left++;
}
next[right] = left;
}
}
int strStr(string haystack, string needle){
int i = 0; // i为文本串指针
int j = -1; // j+1为模板串指针
int next[needle.size()];
getNext(next, needle);
for(; i < haystack.size(); i++){
while(j > -1 && haystack[i] != needle[j+1]){
j = next[j];
}
if(haystack[i] == needle[j+1]){
j++;
}
if(j == needle.size() - 1){
return (i - needle.size() + 1);
}
}
return -1;
}
};
我们当然可以想到暴力解法,即遍历不同长度的前缀,使之与 s 进行匹配,由于子串需要重复,这里我们只需要遍历能被s.size()
整除的长度,并且只需要遍历到 s 长度的一半位置。
上面算法的时间复杂度基本就是 O(n^2)。
如果一个字符串可以由重复的子串组成,那么当两个这样相同的字符串 s 拼接成一个新字符串 t=s+s 时,一定可以在掐头去尾的 t 中找到原字符串 s,也就是肯定可以由原字符串的后缀和前缀拼出一个原字符串。
class Solution{
public:
bool repeatedSubstringPattern(string s){
string t = s + s;
t.erase(t.begin());
t.erase(t.begin() + t.size() - 1);
if(t.find(s) == std::string::npos) return false;
return true;
}
};
库函数find()
的时间复杂度为 O(m+n),不一定是 KMP 算法。本方法的时间复杂度为 O(n)。
KMP 算法本来是用来进行字符串匹配的,与确定重复子串有什么关系呢?先说结论:在重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串。
我们知道当前位置匹配失败时,KMP 算法通过 next 数组来确定回退位置。next 数组就是前缀表,里面是以各个位置为终点的子字符串的最长相等前后缀的长度。如下图所示,最长相等前后缀的性质使得不被包含的子串可以递推地传递下去。
这样就容易判断了,如果不包含的子串长度可以整除原字符串的长度,证明原字符串可以由不包含的子串重复组成。
class Solution{
public:
bool repeatedSubstringPattern(string s){
int len = s.size();
int next[len];
int left = 0;
next[0] = left;
for(int right = 1; right < s.size(); right++){
while(left > 0 && s[left] != s[right]){
left = next[left - 1];
}
if(s[left] == s[right]){
left++;
}
next[right] = left;
}
return next[len - 1] != 0 && len % (len - next[len - 1]) == 0;
}
};