Leetcode 459. 重复的子字符串 Repeated Substring Pattern

本文探讨了如何判断一个字符串是否能由其子串重复构成。通过数学方法和字符串操作,提供了两种解决方案,包括遍历检查周期性和利用S+S的特性。

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

示例 1:

输入: “abab”

输出: True

解释: 可由子字符串 “ab” 重复两次构成。
示例 2:

输入: “aba”

输出: False
示例 3:

输入: “abcabcabcabc”

输出: True

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

链接:459. 重复的子字符串

周期串为s,那么设定t表示周期,值在[1,len(s)-1]的范围,那么剩下的就是依次对每个t值进行
遍历看是否真的满足周期为t,数学中周期的表达式是

f(x+t)=f(x)
​	
那么代码中可以这样写s[i%t]==s[i]

bool repeatedSubstringPattern(string s) {
    int len=s.size(),i=0,t=0;
    for(t=1;t<=len/2;++t)
    {
        if (len%t) continue;    // 有余数,一定不为周期串
        for (i=t;i<len&&s[i%t]==s[i];++i);
        if (i==len) return true;
    }
    return false;
}

上面是数学方法,并且算是遍历,下面是其他方法:假设母串S是由子串s重复N次而成, 则 S+S则有子串s重复2N次, 现在S=Ns, S+S=2Ns 因此S在(S+S)[1:-1]中必出现一次以上

同时注意c++ string.find 函数没找到 返回 string.npos

c.find(s)!=c.npos

    bool repeatedSubstringPattern(string s) {
        string c = s + s;
        c = c.substr(1,c.size()-2);
        if(c.find(s)!=c.npos) return true;
        else 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(n²),取决于标准库实现 - 但实际运行很快,尤其对于小字符串 - 缺点:创建了两倍长度的临时字符串(空间开销) 相比之下,暴力法更容易理解,KMP 法更高效严谨。 但这是一种**优雅的思维巧技(trick)**,适合面试中作为“第二种解法”提出。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值