字符串7——重复的子字符串
字符串7——重复的子字符串
- 例题
- 题目链接
- 题目说明
- 解题
- 方法一:枚举
- 思路与算法
- 代码
- 复杂度分析
- 方法二:字符串匹配
- 思路与算法
- 代码
- 复杂度分析
- 方法三:KMP 算法
- 思路与算法
- 代码
- 复杂度分析
- 正确性证明
- 思考题答案
- 方法四:优化的 KMP 算法
- 思路与算法
- 代码
- 复杂度分析
例题
题目链接
https://leetcode.cn/problems/repeated-substring-pattern/
题目说明
给定一个非空的字符串 s
,检查是否可以通过由它的一个子串重复多次构成。
示例 1:
输入: s = "abab"
输出: true
解释: 可由子串 "ab" 重复两次构成。
示例 2:
输入: s = "aba"
输出: false
示例 3:
输入: s = "abcabcabcabc"
输出: true
解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)
提示:
- 1≤s.length≤1041 \leq s.length \leq1041≤s.length≤104
- s由小写英文字母组成s 由小写英文字母组成s由小写英文字母组成
解题
方法一:枚举
思路与算法
如果一个长度为 nnn 的字符串 sss 可以由它的一个长度为 n′n'n′ 的子串 s′s's′ 重复多次构成,那么:
nnn 一定是 n′n'n′ 的倍数;
s′s's′ 一定是 sss 的前缀;
对于任意的 i∈[n′,n)i \in [n', n)i∈[n′,n),有 s[i]=s[i−n′]s[i] = s[i-n']s[i]=s[i−n′]。
也就是说,sss 中长度为 n′n'n′ 的前缀就是 s′s's′,并且在这之后的每一个位置上的字符 s[i]s[i]s[i],都需要与它之前的第 n′n'n′个字符 s[i−n′]s[i-n']s[i−n′] 相同。
因此,我们可以从小到大枚举 n′n'n′ ,并对字符串 sss 进行遍历,进行上述的判断。注意到一个小优化是,因为子串至少需要重复一次,所以 n′n'n′不会大于 nnn 的一半,我们只需要在 [1,n2][1, \frac{n}{2}][1,2n] 的范围内枚举 n′n'n′ 即可。
代码
C++
class Solution {public:bool repeatedSubstringPattern(string s) {int n = s.size();for (int i = 1; i * 2 <= n; ++i) {if (n % i == 0) {bool match = true;for (int j = i; j < n; ++j) {if (s[j] != s[j - i]) {match = false;break;}}if (match) {return true;}}}return false;}
};
复杂度分析
时间复杂度:O(n2)O(n^2)O(n2),其中 nnn 是字符串 sss 的长度。枚举 iii 的时间复杂度为 O(n)O(n)O(n),遍历 sss 的时间复杂度为 O(n)O(n)O(n),相乘即为总时间复杂度。
空间复杂度:O(1)O(1)O(1)。
方法二:字符串匹配
思路与算法
我们可以把字符串 sss 写成s′s′⋯s′s′s's' \cdots s's's′s′⋯s′s′
的形式,总计 nn′\frac{n}{n'}n′n 个 s′s's′ 。但我们如何在不枚举 n′n'n′ 的情况下,判断 sss 是否能写成上述的形式呢?
如果我们移除字符串 sss 的前 n′n'n′ 个字符(即一个完整的 s′s's′),再将这些字符保持顺序添加到剩余字符串的末尾,那么得到的字符串仍然是 sss。由于 1≤n′<n1 \leq n' < n1≤n′<n,那么如果将两个 sss 连在一起,并移除第一个和最后一个字符,那么得到的字符串一定包含 sss,即 sss 是它的一个子串。
因此我们可以考虑这种方法:我们将两个 sss 连在一起,并移除第一个和最后一个字符。如果 sss 是该字符串的子串,那么 sss 就满足题目要求。
注意到我们证明的是如果 sss 满足题目要求,那么 sss 有这样的性质,而我们使用的方法却是如果 sss 有这样的性质,那么 sss 满足题目要求。因此,只证明了充分性是远远不够的,我们还需要证明必要性。
题解区的很多题解都忽略了这一点,但它是非常重要的。
证明需要使用一些同余运算的小技巧,可以见方法三之后的「正确性证明」部分。这里先假设我们已经完成了证明,这样就可以使用非常简短的代码完成本题。在下面的代码中,我们可以从位置 111 开始查询,并希望查询结果不为位置 nnn,这与移除字符串的第一个和最后一个字符是等价的。
代码
C++
class Solution {public:bool repeatedSubstringPattern(string s) {return (s + s).find(s, 1) != s.size();}
};
python3
class Solution:def repeatedSubstringPattern(self, s: str) -> bool:return (s + s).find(s, 1) != len(s)
复杂度分析
由于我们使用了语言自带的字符串查找函数,因此这里不深入分析其时空复杂度。
方法三:KMP 算法
思路与算法
在方法二中,我们使用了语言自带的字符串查找函数。同样我们也可以自己实现这个函数,例如使用比较经典的 KMP 算法。
读者需要注意以下几点:
KMP 算法虽然有着良好的理论时间复杂度上限,但大部分语言自带的字符串查找函数并不是用 KMP 算法实现的。这是因为在实现 API 时,我们需要在平均时间复杂度和最坏时间复杂度二者之间权衡。普通的暴力匹配算法以及优化的 BM 算法拥有比 KMP 算法更为优秀的平均时间复杂度;
学习 KMP 算法时,一定要理解其本质。如果放弃阅读晦涩难懂的材料(即使大部分讲解 KMP 算法的材料都包含大量的图,但图毕竟只能描述特殊而非一般情况)而是直接去阅读代码,是永远无法学会 KMP 算法的。读者甚至无法理解 KMP 算法关键代码中的任意一行。
由于本题就是在一个字符串中查询另一个字符串是否出现,可以直接套用 KMP 算法。因此这里对 KMP 算法本身不再赘述。读者可以自行查阅资料进行学习。这里留了三个思考题,读者可以在学习完毕后尝试回答这三个问题,检验自己的学习成果:
设查询串的的长度为 nnn,模式串的长度为 mmm,我们需要判断模式串是否为查询串的子串。那么使用 KMP 算法处理该问题时的时间复杂度是多少?在分析时间复杂度时使用了哪一种分析方法?
如果有多个查询串,平均长度为 nnn,数量为 kkk,那么总时间复杂度是多少?
在 KMP 算法中,对于模式串,我们需要预处理出一个 fail\textit{fail}fail 数组(有时也称为 next\textit{next}next 数组、π\piπ数组等)。这个数组到底表示了什么?
代码
C++
class Solution {public:bool kmp(const string& query, const string& pattern) {int n = query.size();int m = pattern.size();vector<int> fail(m, -1);for (int i = 1; i < m; ++i) {int j = fail[i - 1];while (j != -1 && pattern[j + 1] != pattern[i]) {j = fail[j];}if (pattern[j + 1] == pattern[i]) {fail[i] = j + 1;}}int match = -1;for (int i = 1; i < n - 1; ++i) {while (match != -1 && pattern[match + 1] != query[i]) {match = fail[match];}if (pattern[match + 1] == query[i]) {++match;if (match == m - 1) {return true;}}}return false;}bool repeatedSubstringPattern(string s) {return kmp(s + s, s);}
};
复杂度分析
时间复杂度:O(n)O(n)O(n),其中 nnn 是字符串 sss 的长度。
空间复杂度:O(n)O(n)O(n)。
正确性证明
一方面,如果长度为 nnn 的字符串 sss 是字符串 t=s+st=s+st=s+s 的子串,并且 sss 在 ttt 中的起始位置不为 000 或 nnn,那么 sss 就满足题目的要求。证明过程如下:
- 我们设 sss 在 ttt 中的起始位置为 iii,i∈(0,n)i \in (0, n)i∈(0,n)。也就是说,ttt 中从位置 iii 开始的 nnn 个连续的字符,恰好就是字符串 sss。那么我们有:
s[0:n−1]=t[i:n+i−1]s[0:n-1] = t[i:n+i-1]s[0:n−1]=t[i:n+i−1]
由于 ttt 是由两个 sss 拼接而成的,我们可以将 t[i:n+i−1]t[i:n+i-1]t[i:n+i−1] 分成位置 n−1n-1n−1 左侧和右侧两部分:
{s[0:n−i−1]=t[i:n−1]s[n−i:n−1]=t[n:n+i−1]=t[0:i−1]\left \{ \begin{aligned} s[0:n-i-1] &= t[i:n-1] \\ s[n-i:n-1] &= t[n:n+i-1] = t[0:i-1] \end{aligned} \right.{s[0:n−i−1]s[n−i:n−1]=t[i:n−1]=t[n:n+i−1]=t[0:i−1]
每一部分都可以对应回 sss:
{s[0:n−i−1]=s[i:n−1]s[n−i:n−1]=s[0:i−1]\left \{ \begin{aligned} s[0:n-i-1] &= s[i:n-1] \\ s[n-i:n-1] &= s[0:i-1] \end{aligned} \right.{s[0:n−i−1]s[n−i:n−1]=s[i:n−1]=s[0:i−1]
这说明,sss 是一个「可旋转」的字符串:将 ss 的前 ii 个字符保持顺序,移动到 sss 的末尾,得到的新字符串与 ss 相同。也就是说,在模 nnn 的意义下,
s[j]=s[j+i]s[j] = s[j+i]s[j]=s[j+i]
对于任意的 jjj 恒成立。
「在模 nnn 的意义下」可以理解为,所有的加法运算的结果都需要对 nnn 取模,使得结果保持在 [0,n)[0, n)[0,n) 中,这样加法就自带了「旋转」的效果。
如果我们不断地连写这个等式:
s[j]=s[j+i]=s[j+2i]=s[j+3i]=⋯s[j] = s[j+i] = s[j+2i] = s[j+3i] = \cdotss[j]=s[j+i]=s[j+2i]=s[j+3i]=⋯
那么所有满足 j0=j+k⋅ij_0 = j + k \cdot ij0=j+k⋅i的位置 j0j_0j0 都有 s[j]=s[j0]s[j] = s[j_0]s[j]=s[j0],jjj 和 j0j_0j0
在模 iii 的意义下等价。由于我们已经在模 nnn 的意义下讨论这个问题,因此 jjj 和 j0j_0j0
在模 gcd(n,i)\mathrm{gcd}(n, i)gcd(n,i) 的意义下等价,其中 gcd\mathrm{gcd}gcd 表示最大公约数。也就是说,字符串 ss 中的两个位置如果在模 gcd(n,i)\mathrm{gcd}(n, i)gcd(n,i) 的意义下等价,那么它们对应的字符必然是相同的。
由于 gcd(n,i)\mathrm{gcd}(n, i)gcd(n,i) 一定是 nnn 的约数,那么字符串 sss 一定可以由其长度为 gcd(n,i)\mathrm{gcd}(n, i)gcd(n,i)的前缀重复 ngcd(n,i)\frac{n}{\mathrm{gcd}(n, i)}gcd(n,i)n次构成。
另一方面,如果 sss 满足题目的要求,那么 sss包含若干个「部分」,t=s+st=s+st=s+s包含两倍数量的「部分」,因此 sss 显然是 ttt 的子串,并且起始位置可以不为 000 或 nnn:我们只需要选择 ttt 中第一个「部分」的起始位置即可。
综上所述,我们证明了:长度为 nnn 的字符串 sss 是字符串 t=s+st=s+st=s+s 的子串,并且 sss 在 ttt 中的起始位置不为 000 或 nnn,当且仅当 sss 满足题目的要求。因此,
思考题答案
设查询串的的长度为 nnn,模式串的长度为 mmm,我们需要判断模式串是否为查询串的子串。那么使用 KMPKMPKMP 算法处理该问题时的时间复杂度是多少?在分析时间复杂度时使用了哪一种分析方法?
时间复杂度为 O(n+m)O(n+m)O(n+m),用到了均摊分析(摊还分析)的方法。
具体地,无论在预处理过程还是查询过程中,虽然匹配失败时,指针会不断地根据 fail\textit{fail}fail 数组向左回退,看似时间复杂度会很高。但考虑匹配成功时,指针会向右移动一个位置,这一部分对应的时间复杂度为 O(n+m)O(n+m)O(n+m)。又因为向左移动的次数不会超过向右移动的次数,因此总时间复杂度仍然为 O(n+m)O(n+m)O(n+m)。
如果有多个查询串,平均长度为 nnn,数量为 kkk,那么总时间复杂度是多少?
时间复杂度为 O(nk+m)O(nk+m)O(nk+m)。模式串只需要预处理一次。
在 KMP 算法中,对于模式串,我们需要预处理出一个 fail\textit{fail}fail 数组(有时也称为 next\textit{next}next 数组、π\piπ数组等)。这个数组到底表示了什么?
fail[i]\textit{fail}[i]fail[i] 等于满足下述要求的 xxx 的最大值:s[0:i]s[0:i]s[0:i] 具有长度为 x+1x+1x+1的完全相同的前缀和后缀。这也是 KMP 算法最重要的一部分。
方法四:优化的 KMP 算法
思路与算法
如果读者能够看懂「正确性证明」和「思考题答案」这两部分,那么一定已经发现了方法三中的 KMP 算法有可以优化的地方。即:
在「正确性证明」部分,如果我们设 iii 为最小的起始位置,那么一定有 gcd(n,i)=i\mathrm{gcd}(n, i) = igcd(n,i)=i,即 nnn 是 iii 的倍数。这说明字符串 sss 是由长度为 iii 的前缀重复 ni\frac{n}{i}in次构成;
由于 fail[n−1]\textit{fail}[n-1]fail[n−1] 表示 sss 具有长度为 fail[n−1]+1\textit{fail}[n-1]+1fail[n−1]+1 的完全相同的(且最长的)前缀和后缀。那么对于满足题目要求的字符串,一定有 fail[n−1]=n−i−1\textit{fail}[n-1] = n-i-1fail[n−1]=n−i−1,即 i=n−fail[n−1]−1i = n - \textit{fail}[n-1] - 1i=n−fail[n−1]−1;
对于不满足题目要求的字符串,nnn 一定不是 n−fail[n−1]−1n - \textit{fail}[n-1] - 1n−fail[n−1]−1 的倍数。
上述所有的结论都可以很容易地使用反证法证出。
因此,我们在预处理出 fail\textit{fail}fail 数组后,只需要判断 nnn 是否为 n−fail[n−1]−1n - \textit{fail}[n-1] - 1n−fail[n−1]−1的倍数即可。
代码
C++
class Solution {public:bool kmp(const string& pattern) {int n = pattern.size();vector<int> fail(n, -1);for (int i = 1; i < n; ++i) {int j = fail[i - 1];while (j != -1 && pattern[j + 1] != pattern[i]) {j = fail[j];}if (pattern[j + 1] == pattern[i]) {fail[i] = j + 1;}}return fail[n - 1] != -1 && n % (n - fail[n - 1] - 1) == 0;}bool repeatedSubstringPattern(string s) {return kmp(s);}
};
复杂度分析
时间复杂度:O(n)O(n)O(n),其中 nnn 是字符串 sss 的长度。
空间复杂度:O(n)O(n)O(n)。
字符串7——重复的子字符串相关推荐
- js实现kmp算法_「leetcode」459.重复的子字符串:KMP算法还能干这个!
不瞒你说,重复子串问题,KMP很拿手 题目459.重复的子字符串 给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成.给定的字符串只含有小写英文字母,并且长度不超过10000. 示例 1: ...
- 力扣459. 重复的子字符串(KMP,JavaScript)
如果 next[len - 1] != 0,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度). 最长相等前后缀的长度为:next[len - 1] . 数组长度为:le ...
- 算法Day8|字符串专题二 剑指 Offer 58 - II. 左旋转字符串,28. 找出字符串中第一个匹配项的下标,459. 重复的子字符串
剑指 Offer 58 - II. 左旋转字符串 解题思路: 反转区间为前n的子串 反转区间为n到末尾的子串 反转整个字符串 class Solution {public String reverse ...
- 【字符串】leet459.重复的子字符串(C/C++/Java/Python/Js)
leetcode459.重复的子字符串 1 题目 2 思路 3 代码 3.1 C++版本 3.2 C版本 3.3 Java版本 3.4 Python版本 3.5 JavaScript版本 4 总结 K ...
- 字符串专题-LeetCode:剑指 Offer 58 - II. 左旋转字符串、LeetCode 459.重复的子字符串、 代码思路和注意点
文章目录 一.剑指 Offer 58 - II. 左旋转字符串 二.LeetCode 459.重复的子字符串 一.剑指 Offer 58 - II. 左旋转字符串 思路: 预留出n个字符空间s.res ...
- 402-字符串(题目:剑指Offer58-II.左旋转字符串、 28. 实现 strStr()、459.重复的子字符串)
题目:剑指Offer58-II.左旋转字符串 class Solution {public:string reverseLeftWords(string s, int n) {string s1(s. ...
- 随想录Day9--28. 实现 strStr() , 459.重复的子字符串
今天的两道题关键在于学习KMP算法.KMP算法运用场景在于一串字符串里面查找是否含有某个子字符串,如"abcdef"里面就含有"cdf"这么个子字符串.先把题目 ...
- 【代码随想录二刷】day9 | 28. 实现 strStr() 459.重复的子字符串
二刷主要记录理解不一样的题 一刷地址:day9 今日题目:中等 KMP:困难 => 第一时间想到了使用KMP,但是不太会,只有用常规方法完成 实现 strStr():拼接完两个字符串s之后,取其 ...
- 代码随想录Day09:28. 实现 strStr()、459.重复的子字符串、字符串总结 、双指针回顾
目录 Day09:28. 实现 strStr().459.重复的子字符串.字符串总结 .双指针回顾 28. 实现 strStr() (一刷只看了思想) 459.重复的子字符串 (本题一刷跳过了) 字符 ...
最新文章
- ubuntu18.04 出现 Command ‘ifconfig‘ not found 问题的解决办法
- matlab模拟风场竖桥向时程,索梁结构应急桥抖振响应分析
- 关于java的对象数组
- word 插入代码_Word教程:最神奇的快捷键:Alt+X,一秒变出各种符号!
- dotNET Core 中怎样操作 AD?
- 前端学习(2966):上午回顾
- Series与DataFrame数据类型操作基础
- 二分法的样例 题解
- 安卓平台中的动态加载技术分析
- 深圳保障性住房【公租房、安居房、人才房】简单说明
- vmware 桌面 服务器版,vmware云桌面软件服务器(vmware云桌面搭建教程)
- IOS美图秀秀(滤镜和涂鸦)和 添加阴影功能
- MyBatis入门回顾
- 短信截取 android,谷歌Android增加语音操作功能 可语音发送短信
- 图书管理系统 jsp + servlet + mysql (2023)
- css中white-space的值pre-wrap
- HTML入门习题及答案
- Win8.1打开电脑时提示C:\WINDOWS\system32\config\systemprofile\Desktop不可用的解决方法
- spark原理之一张图搞定broadcast
- 一图看懂centos和ubuntu命令区别