LC459. 重复的子字符串
给定一个非空的字符串 s
,检查是否可以通过由它的一个子串重复多次构成。
示例 1:
输入: s = “abab”
输出: true
解释: 可由子串 “ab” 重复两次构成。
示例 2:
输入: s = “aba”
输出: false
示例 3:
输入: s = “abcabcabcabc”
输出: true
解释: 可由子串 “abc” 重复四次构成。 (或子串 “abcabc” 重复两次构成。)
提示:
- 1 ≤ s . l e n g t h ≤ 1 0 4 1 \leq s.length \leq 10^4 1≤s.length≤104
s
由小写英文字母组成
解法一(枚举)
思路分析:
-
长度为n的字符串可以由长度为
m
的子串t
重复多次构成,则存在以下关系:-
n 一定是
m
的倍数 -
子串
t
一定是s的前缀 -
对于任意 i ∈ [ m , n ) i \in [m, n) i∈[m,n),有
s[i] = s[i-m]
-
-
即字符串
s
中长度为m
的前缀就是t
,且在这之后的每一个位置上的字符s[i]
,都需要与该位置之前的m
个字符s[i-m]
相等 -
且因为字符串
s
由某个字串构成,即子串至少需要重复一次,即m
不会大于n
的一半,所以只需要在 [ 1 , n 2 ] [1, \frac{n}{2}] [1,2n]范围内遍历即可
实现代码如下:
class Solution {
public boolean repeatedSubstringPattern(String s) {
int n = s.length();
for (int i = 1; i <= n/2; ++i) { // i指子串长度
if (n % i == 0) {
boolean flag = true;
for (int j = i; j < n; ++ j) { // 遍历判断字符串s是否由长度为i的子串构成
if (s.charAt(j) != s.charAt(j-i)) {
flag = false;
break;
}
}
if (flag) return true; // 出现一个满足条件的子串 则返回true
}
}
return false;
}
}
提交结果如下:
解答成功:
执行耗时:9 ms,击败了78.70% 的Java用户
内存消耗:43.5 MB,击败了5.02% 的Java用户
复杂度分析:
-
时间复杂度: O ( m × n ) O(m \times n) O(m×n),遍历寻找符合条件的子串长度需要时间复杂度为 O ( m ) O(m) O(m),判断子串是否符合需要时间复杂度为 O ( n ) O(n) O(n)
-
空间复杂度: O ( 1 ) O(1) O(1),使用了常量空间
解法二(字符串匹配+Java API)
思路分析:
-
若字符串
s
可以写成 s ′ s ′ s ′ ⋯ s ′ s ′ s's's'\cdots s's' s′s′s′⋯s′s′形式,即由若干个重复子串构成,即将第一个子串 s ′ s' s′,移动到字符串的末尾重新组成一个字符串,且有新组成的字符串依然还是等于字符串s
-
同理,把两个字符串
s
联合组到一起,并移除第一个元素和最后一个元素,那么得到的字符串一定包含s
,即s
是联合组成的新字符串的一个子串 -
即
s
由重复子串构成,则s
有第二点提到的性质,这证明了充分性,而解题需要证明必要性,即s
有第二点的性质,则s
由重复子串构成 -
如何证明必要性;即有字符串
t = s+s
,且s
在t
中的起始位置不为0或n,那么s
就满足题目要求 -
详细证明如:详细证明
实现代码如下:
class Solution {
public boolean repeatedSubstringPattern(String s) {
return (s+s).indexOf(s, 1) != s.length();
}
}
提交结果如下:
解答成功:
执行耗时:82 ms,击败了38.79% 的Java用户
内存消耗:43.8 MB,击败了5.04% 的Java用户
复杂度分析:
- 语言自带字符串查找函数,不具体分析
解法三(字符串匹配+KMP算法)
思路分析:
-
对于解法二中,查询字符串函数可以使用KMP算法来实现
-
且next数组对应于 字符串
s
的前缀表右移一步
实现代码如下:
class Solution {
public boolean repeatedSubstringPattern(String s) {
return kmp(s+s, s);
}
private boolean kmp(String s, String t) {
int sLen = s.length(); // 字符串s的长度
int tLen = t.length(); // 字符串t的长度
int[] next = new int[tLen]; // 创建next数组
// 初始化next数组
int j = -1;
next[0] = j;
for (int i = 1; i < tLen; ++i) {
while (j >= 0 && t.charAt(i) != t.charAt(j+1)) {
j = next[j];
}
if (t.charAt(i) == t.charAt(j+1))
++ j;
next[i] = j;
}
// 判断字符串t是否在字符串s中
j = -1; // 与初始化next数组时对应
for (int i = 1; i < sLen-1; ++i) { // 只能判断子串t是否在字符串s[1:n-1)中
while (j >= 0 && s.charAt(i) != t.charAt(j+1)) {
j = next[j];
}
if (s.charAt(i) == t.charAt(j+1)) {
++ j;
}
if (j == tLen-1) {
return true;
}
}
return false;
}
}
提交结果如下:
解答成功:
执行耗时:15 ms,击败了49.46% 的Java用户
内存消耗:43.9 MB,击败了5.04% 的Java用户
复杂度分析:
-
时间复杂度: O ( n ) O(n) O(n),遍历字符串
s
和组成的字符串t
-
空间复杂度: O ( n ) O(n) O(n),next数组花费空间复杂度为 O ( n ) O(n) O(n)
解法四(优化解法三)
思路分析:
-
综合解法三和解法二的思路,可以发现 字符串由长度为
i
的前缀重复 n i \frac{n}{i} in次构成,即设i
为最小的起始位置,即gcd(n,i) = i
-
且对于数组
next[n-1]
表示s
具有长度为next[n-1]
的相同的前缀和后缀,那么对于满足条件的字符串一定有next[n-1] = n-i
即i = n-next[n-1]
-
即满足条件则
n
是n-next[n-1]
的倍数
实现代码如下:
class Solution {
public boolean repeatedSubstringPattern(String s) {
int n = s.length(); // 字符串s的长度
int[] next = new int[n]; // next数组对应s的前缀表 右移
int j = -1;
next[0] = j;
for (int i = 1; i < n; ++i) {
while (j >= 0 && s.charAt(i) != s.charAt(j+1)) {
j = next[j];
}
if (s.charAt(i) == s.charAt(j+1)) {
++ j;
}
next[i] = j;
}
// 因为next数组对应s的前缀表右移一位
// 所以next[n-1]表示的是s具有长度为next[n-1]的相同前后缀的值-1
// 所以计算时字符串s相同前后缀的长度为 next[n-1]+1
// 所以判断 n 与 n-next[n-1]-1
// 且需要注意next[n-1]+1 应该大于等于0 保证字符串s具有相同的前后缀
return next[n-1] >= 0 && n % (n - next[n - 1] - 1) == 0;
}
}
提交结果如下:
解答成功:
执行耗时:8 ms,击败了83.28% 的Java用户
内存消耗:44 MB,击败了5.04% 的Java用户
复杂度分析:
-
时间复杂度: O ( n ) O(n) O(n),只需遍历一遍字符串s
-
空间复杂度: O ( n ) O(n) O(n),需要使用辅助数组next