【leetcode刷题】[简单]459. 重复的子字符串(repeated substring pattern)-java

本文介绍了一种判断字符串是否能由其子串重复构成的方法。通过构造新的字符串并检查原字符串是否为新字符串的子串来实现。适用于字符串长度不大于10000的情况。

重复的子字符串 repeated substring pattern

题目

给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。

示例 1:

输入: "abab"

输出: True

解释: 可由子字符串 "ab" 重复两次构成。

示例 2:

输入: "aba"

输出: False

示例 3:

输入: "abcabcabcabc"

输出: True

解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。)

代码模板:

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        
    }
}

分析

把字符串一个去掉首拼上这个字符串去掉尾的,如果contains这个字符串,说明是重复子字符串。

解答

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        StringBuffer newStr = new StringBuffer(s.substring(1,s.length()));
        newStr.append(s.substring(0,s.length()-1));
        if(newStr.toString().contains(s)){
            return true;
        }
        return false;
    }
}
这份代码是 LeetCode 官方提供的一种**非常巧妙的数学+字符串拼接解法**,虽然只有短短一行,但背后原理深刻。我们来逐行解释它的 **逻辑、正确性证明和背后的直觉**。 --- ### ✅ 代码全文 ```cpp class Solution { public: bool repeatedSubstringPattern(string s) { return (s + s).find(s, 1) != s.size(); } }; ``` --- ## 🎯 目标问回顾 判断一个字符串 `s` 是否可以由它的一个**非平凡子串(长度 < n)重复多次构成**。 例如: - `"abab"` → 是,由 `"ab"` 重复两次 - `"abcabc"` → 是,由 `"abc"` 重复 - `"abcabd"` → 否 - `"aaaa"` → 是,由 `"a"` 重复四次 --- ## 🔍 核心思想:**双倍字符串技巧** 我们将原字符串 `s` 拼接成 `s + s`,然后从第二个字符开始查找原始 `s` 是否出现。 > 即:在 `(s+s)` 中找 `s`,但跳过第一个位置(从索引 1 开始搜) 如果能找到,并且找到的位置不是 `s.size()`,说明有重复结构。 --- ## 🧠 直观理解(举个例子) 设 `s = "abab"` ### 第一步:构造 `s + s` ``` s + s = "abababab" 01234567 ``` 现在我们要在 `s+s` 中从下标 `1` 开始搜索子串 `s = "abab"` 即调用: ```cpp (s + s).find(s, 1) ``` 这表示:从第 1 个位置开始找 `"abab"` 出现的位置。 我们看看有哪些可能匹配: ``` abababab abab ← 从位置 1 开始?不匹配('b' vs 'a') bab → 不行 abab ← 在位置 2 匹配成功! ``` 所以 `.find(s, 1)` 返回 `2` 而 `s.size() == 4` 判断: ```cpp 2 != 4 → true ``` ✅ 返回 `true` —— 正确! --- ## ❓ 为什么这样能工作? ### 假设 `s` 可以被某个子串 `t` 重复 k 次得到: > `s = t + t + ... + t = t * k` (k ≥ 2) 那么: - `s + s = (t * k) + (t * k) = t * (2k)` - 这是一个更长的周期性字符串 我们现在从中间切开,在 `s+s` 中去掉开头的一个字符后,仍然应该能“对齐”出一个完整的 `s` 关键观察: > 如果 `s` 有重复结构,则 `s` 必然会在 `(s+s)` 的内部(非首尾)再次出现。 --- ## 📌 关键点:`.find(s, 1)` 的含义 - `(s+s).find(s, 1)` 表示:在 `s+s` 中从下标 `1` 开始寻找 `s` 第一次出现的位置。 - 它不会匹配最前面的那个完整 `s`(因为从 1 开始),所以是在找“是否还能再拼出一个 `s`”。 --- ## ✅ 正确性的两种情况分析 ### ✅ 情况一:`s` 真的是重复子串构成的 如 `s = "abcabc"` `s + s = "abcabcabcabc"` 从位置 1 开始找 `"abcabc"`: ``` abcabcabcabc ↑↑↑ 找到!在位置 3 就能重新对齐 ``` → `.find(...)` 返回 `3` → `3 != 6`(`s.size()`) → 返回 `true` ✅ ### ✅ 情况二:`s` 不是重复子串构成的 比如 `s = "abcabd"` `s + s = "abcabdabcabd"` 从位置 1 开始找 `"abcabd"`,会发生什么? 尝试所有偏移都不行,直到跳过前半部分 `abcabd` 后才能找到下一个完整 `s`,也就是在位置 `6` → `.find(...)` 返回 `6` → `6 == s.size()` → 条件为假 → 返回 `false` ✅ --- ## 🧩 数学解释(进阶) 令 `n = s.size()` 考虑字符串 `T = s + s` 如果我们能在 `T` 中从位置 `i ∈ [1, n-1]` 找到 `s`,说明: > `T.substr(i, n) == s` 这意味着:把两个 `s` 拼起来之后,移动 `i` 位又能得到原串 → 存在循环移位等于自身 → 说明具有周期性 → 存在重复子串。 而只有当 `s` 具有周期结构时,才会在 `[1, n-1]` 内找到 `s`。 否则,第一次出现只能在 `n` 处(即第二个完整的 `s` 开头)。 因此: ```cpp (s + s).find(s, 1) < s.size() ⇔ s 有重复子串模式 ``` 但我们写成: ```cpp != s.size() ``` 是因为 `.find()` 找不到会返回 `string::npos`,但在本中总会找到(至少在 `n` 处有一个),所以我们只需排除“只在 `n` 处找到”的情况。 > 所以只要不是在 `s.size()` 处才第一次出现,就说明有更早的匹配 → 有重复结构。 --- ## ⚠️ 边界注意:单字符情况 比如 `s = "a"` - `s + s = "aa"` - 查找 `s="a"` 从位置 1 开始 → 在位置 1 能找到? - 但是 `"aa".find("a", 1)` 返回 `1` - `s.size() == 1` - `1 != 1`?❌ 错了! 等等!`1 == 1` → 判断失败 → 返回 `false` 但实际上 `"a"` 是不能由“比它短的子串”重复而来(需要至少重复两次,子串必须真小于原串) 所以 `"a"` 应该返回 `false` ✅ 这个方法自动处理了这一点! --- ## ✅ 总结:一句话解释 > 将字符串 `s` 拼接成 `s+s`,如果在去掉头部的情况下(从第2个字符开始)仍能找到完整的 `s`,说明 `s` 具有周期性结构 —— 即可由某个子串重复构成。 --- ## 💡 类比记忆:旋转词检测 这个技巧类似于判断两个字符串是否互为旋转: > `s2` 是 `s1` 的旋转 ⇔ `s1` 是 `s1+s1` 的子串 同理,这里是: > `s` 有重复子串 ⇔ `s` 在 `(s+s)[1:-1]` 中出现(即去掉首尾一小段后的中间部分) 虽然代码没显式切片,但 `.find(s, 1)` 实现了类似效果。 --- ## 📈 时间复杂度 & 局限性 - `.find()` 在 C++ 中通常是 O(n) 平均时间(KMP 或优化实现),最坏可能 O(),取决于标准库实现 - 但实际运行很快,尤其对于小字符串 - 缺点:创建了两倍长度的临时字符串(空间开销) 相比之下,暴力法更容易理解,KMP 法更高效严谨。 但这是一种**优雅的思维巧技(trick)**,适合面试中作为“第二种解法”提出。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值