LeetCode 459. 重复的子字符串
题目描述

题意分析
要判断的是:能不能找到一个非空子串,把它连续重复若干次(至少两次)后正好拼成原字符串。返回值只有真假两种,不需要给出那个子串本身。
有三个隐含条件必须先读出来:这个子串必须是原串的前缀,它的长度必须整除原串长度n,并且长度不能等于n——否则「重复一次」对任何字符串都成立,题目就没有意义了。
约束信号是长度上限一万且只含小写字母。一万这个量级意味着 $O(n^2)$ 级别的枚举也能通过,但出题人显然是想看有没有更漂亮的判定方式,所以这题在面试里几乎总会被追问「还有别的做法吗」。
边界:长度为 1 的字符串一定返回假,因为唯一的非空真前缀不存在;"aa"这种最短的合法情形要返回真;全部字符相同的串永远返回真;而像"aba"这种长度为素数、首尾对称的串很容易骗过粗糙的判断,是必查的反例。
解法:拼接字符串去首尾判断
核心思路
若
s由某个更短子串重复得到,那么把s向左旋转一个循环节的长度,结果仍是s。而s + s的所有长度为n的子串,恰好对应s的各种旋转。
s在s + s的下标0和n处一定出现,它们只是“旋转 0 位”的平凡匹配,不能作为答案。因此删掉拼接串的首尾字符,只保留下标1..n-1对应的非零旋转,再判断其中是否包含s:
s 是重复子串构成 <=> (s + s)[1, 2n - 1) 包含 s正向来看,若
s = t^k且k >= 2,从下标|t|开始仍能读出完整的s,这个位置位于保留区间。反向来看,若s在某个1 <= i < n的位置匹配,说明旋转i位后字符串不变,因此字符按周期循环,s可由长度gcd(i, n)的前缀重复得到;该长度严格小于n。两个方向都成立,所以判定充分且必要。
解题步骤
- 拼接得到
doubled = s + s。- 去掉
doubled的第一个和最后一个字符,排除下标0、n的平凡匹配。- 在剩余字符串中查找
s:找到说明存在非零旋转周期,返回true;否则返回false。例如
s = "abab",doubled = "abababab",去掉首尾得到"bababa",其中包含"abab",所以返回true。对于"aba",中间串是"baab",不包含原串,返回false。单字符"a"的中间串为空,也会正确返回false。
代码实现
class Solution {
public boolean repeatedSubstringPattern(String s) {
String doubled = s + s;
return doubled.substring(1, doubled.length() - 1).contains(s);
}
}
import "strings"
func repeatedSubstringPattern(s string) bool {
doubled := s + s
return strings.Contains(doubled[1:len(doubled)-1], s)
}
复杂度分析
- 时间复杂度:构造中间串需要
O(n);子串查找取决于标准库实现,使用线性匹配算法时为O(n),朴素匹配的最坏情况为O(n^2)。若面试要求严格线性上界,可用 KMP 完成同一次查找。- 空间复杂度:
O(n),用于保存拼接字符串及中间串。
关键点总结
- “由子串重复构成”等价于“经过某次非零旋转后仍等于自身”。
s + s展开了所有旋转;删除首尾是为了同时排除两次原位匹配。- 不需要额外枚举循环节长度,非零旋转匹配已经隐含了周期与整除关系。
- 面试中应能证明充要性,并主动说明库函数查找的复杂度依赖实现;若被限制不能调用库函数,再换 KMP。
易错点总结
- 直接在
s + s中查找s,任何字符串都会在下标0匹配,结果恒为真。- 只删除首字符:下标
n处仍保留一份完整原串,例如"ab"会被误判为真;首尾必须各删一个字符。- 枚举候选循环节时,必须同时满足长度小于
n且能整除n,否则字符串自身或不完整重复会被误判。- 单字符没有非空真子串,必须返回
false;首尾裁剪后的空串会自然处理该边界。- 复杂度不能无条件写成
O(n),因为contains/Contains的最坏上界取决于具体标准库匹配实现。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 28. 找出字符串中第一个匹配项的下标 | 简单 | 要返回匹配下标而非真假,是手写 KMP 与 next 数组的正题 |
| 796. 旋转字符串 | 简单 | 同样用 s + s,但不砍首尾,还需先比较两串长度是否相等 |
| 面试题 01.09. 字符串轮转 | 简单 | 与 796 同解法换皮,限定只能调用一次判断子串的方法 |