leetcode 459.重复的子字符串
题目链接:重复的子字符串
方法一:移动匹配
判断字符串s是否有重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是有重复子串组成。
但是,当我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+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;
return false;
}
};
说明:本题采用移动匹配的方法,最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,直接用了contains,find 之类的库函数, 而实现这些函数的时间复杂度较大(暴力解法是m * n,一般库函数实现为 O(m + n)),KMP算法可以相对高效的实现算法。
方法二:KMP算法
在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串。
简单推理
(1)假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。
(2)因为字符串s的最长相同前后缀的的长度一定是不包含s本身,所以最长相同前后缀长度必然是m * x,而且 n - m = 1,所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串。
(3)next 数组记录的就是最长相同前后缀字符串:如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。
(4)最长相等前后缀的长度为:next[len - 1] + 1。(这里的next数组是以统一减一的方式计算的,因此需要+1)
(5)数组长度为:len。如果len % (len - (next[len - 1] + 1)) == 0 ,则说明数组的长度正好可以被 (数组长度-最长相等前后缀的长度) 整除 ,说明该字符串有重复的子字符串。
数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
- 前缀表统一减一的方式:
class Solution {
public:
void getNext (int* next, const string& s){
next[0] = -1;
int j = -1;
for(int i = 1;i < s.size(); i++){
while(j >= 0 && s[i] != s[j + 1]) {
j = next[j];
}
if(s[i] == s[j + 1]) {
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] != -1 && len % (len - (next[len - 1] + 1)) == 0) {
return true;
}
return false;
}
};
- 前缀表(不减一)的实现方式:
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;
}
};