459. 重复的子字符串
给定一个非空的字符串 s
,检查是否可以通过由它的一个子串重复多次构成。
示例1:
输入: s = "abab"
输出: true
解释: 可由子串 "ab" 重复两次构成。
示例2:
输入: s = "aba"
输出: false
示例3:
输入: s = "abcabcabcabc"
输出: true
解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)
思路
暴力解法
一开始的思路是用暴力解法,先用2个for,一个找子字符串的一个for获取子串起始位置,一个for获取子串结束位置,一个个找出原字符串的子串,然后再用for寻找判断子串是否能重复构成原字符串。
再看讲解后发现暴力解法只用两个for,再读题发现,是整个字符串都由重复的子串构成,而不是存在重复的字串。所以如果有重复子串的话,从原字符串的起始第一个字符就要是重复子串的开头,因此只用1个for循环获取子串的终止位置就可以然后判断子串是否能重复构成字符串,又嵌套一个for循环,是O(n^2)的时间复杂度
难点:KMP
因为KMP能做到的就是在一个串中查找是否出现过另一个串,现在在一个串中找重复的子串也是能用KMP。
我们要用到前缀表next[],前缀表里统计了各个位置为终点字符串的最长相同前后缀的长度,我们要找的是原字符串s的最长相同前后缀和重复子串的关系
- 数学推导:
假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。
因为字符串s的最长相同前后缀的的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且 n - m = 1,(这里如果不懂,看上面的推理)
所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串。
其中next 数组记录的就是最长相同前后缀, 如果 next[len-1] != -1,则说明字符串有最长相同的前后缀
如果len % (len - next[len - 1]) == 0 ,则说明数组的长度正好可以被 (数组长度-最长相等前后缀的长度) 整除 ,说明该字符串有重复的子字符串。
数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
完整代码如下(next不减一的方式)
public class RepeatedSubstringPattern {
public boolean repeatedSubstringPattern(String s) {
// 定义前缀数组next
int[] next = new int[s.length()];
// 得到前缀表
getNext(next, s);
// 判断是否能整除重复子串
if (next[s.length()-1] != 0 && (s.length() % (s.length() - next[s.length() - 1])) == 0) {
return true;
}
return false;
}
// 定义获得前缀表不减一的next数组的方法
public void getNext(int[] next, String s) {
// 初始化
int j = 0; // 相同前缀的末尾
next[0] = j;
// i是相同后缀的末尾
for (int i = 1; i < s.length(); i++) {
// 处理不相等的情况
while (j > 0 && s.charAt(i) != s.charAt(j)) {
j = next[j - 1];
}
// 处理相等的情况
if (s.charAt(i) == s.charAt(j)) {
j++; // 因为next数组存的是长度,此时j和i指向前后缀的最后一个字符,j还要+1表长度
}
next[i] = j;
}
}
}