Expand Up
@@ -56,10 +56,66 @@

所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。
当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,**要刨除 s + s 的首字符和尾字符**,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。
以上证明的充分性,接下来证明必要性:
如果有一个字符串s,在 s + s 拼接后, 不算首尾字符,如果能凑成s字符串,说明s 一定是重复子串组成。
如图,字符串s,图中数字为数组下标,在 s + s 拼接后, 不算首尾字符,中间凑成s字符串。

图中,因为中间拼接成了s,根据红色框 可以知道 s[4] = s[0], s[5] = s[1], s[0] = s[2], s[1] = s[3] s[2] = s[4] ,s[3] = s[5]

以上相等关系我们串联一下:
s[4] = s[0] = s[2]
s[5] = s[1] = s[3]
即:s[4],s[5] = s[0],s[1] = s[2],s[3]
**说明这个字符串,是由 两个字符 s[0] 和 s[1] 重复组成的**!
这里可以有录友想,凭什么就是这样组成的s呢,我换一个方式组成s 行不行,如图:

s[3] = s[0],s[4] = s[1] ,s[5] = s[2],s[0] = s[3],s[1] = s[4],s[2] = s[5]
以上相等关系串联:
s[3] = s[0]
s[1] = s[4]
s[2] = s[5]
s[0] s[1] s[2] = s[3] s[4] s[5]
和以上推导过程一样,最后可以推导出,这个字符串是由 s[0] ,s[1] ,s[2] 重复组成。
如果是这样的呢,如图:

s[1] = s[0],s[2] = s[1] ,s[3] = s[2],s[4] = s[3],s[5] = s[4],s[0] = s[5]
以上相等关系串联
s[0] = s[1] = s[2] = s[3] = s[4] = s[5]
最后可以推导出,这个字符串是由 s[0] 重复组成。
以上 充分和必要性都证明了,所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。
代码如下:
```CPP
Expand All
@@ -76,13 +132,14 @@ public:
* 时间复杂度: O(n)
* 空间复杂度: O(1)
不过这种解法还有一个问题,就是 我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数。 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))。
不过这种解法还有一个问题,就是 我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数, 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))。
如果我们做过 [28.实现strStr](https://programmercarl.com/0028.实现strStr.html) 题目的话,其实就知道,**实现一个 高效的算法来判断 一个字符串中是否出现另一个字符串是很复杂的**,这里就涉及到了KMP算法。
### KMP
#### 为什么会使用KMP
以下使用KMP方式讲解,强烈建议大家先把以下两个视频看了,理解KMP算法,再来看下面讲解,否则会很懵。
* [视频讲解版:帮你把KMP算法学个通透!(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/)
Expand All
@@ -91,7 +148,9 @@ public:
在一个串中查找是否出现过另一个串,这是KMP的看家本领。那么寻找重复子串怎么也涉及到KMP算法了呢?
KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一个匹配过的位置继续匹配,靠的是有计算好的前缀表。 前缀表里,统计了各个位置为终点字符串的最长相同前后缀的长度。
KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一个匹配过的位置继续匹配,靠的是有计算好的前缀表。
前缀表里,统计了各个位置为终点字符串的最长相同前后缀的长度。
那么 最长相同前后缀和重复子串的关系又有什么关系呢。
Expand All
@@ -100,16 +159,61 @@ KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一
* 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;
* 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里拿字符串s:abababab 来举例,ab就是最小重复单位,如图所示:
#### 充分性证明
如果一个字符串s是由重复子串组成,那么 最长相等前后缀不包含的子串一定是字符串s的最小重复子串。
证明: 如果s 是有是有最小重复子串p组成。
即 s = n * p
那么相同前后缀可以是这样:

也可以是这样:

最长的相等前后缀,也就是这样:

这里有录友就想:如果字符串s 是有是有最小重复子串p组成,最长相等前后缀就不能更长一些? 例如这样:


如果这样的话,因为前后缀要相同,所以 p2 = p1,p3 = p2,如图:
#### 如何找到最小重复子串

这里有同学就问了,为啥一定是开头的ab呢。 其实最关键还是要理解 最长相等前后缀,如图:
p2 = p1,p3 = p2 即: p1 = p2 = p3

说明 p = p1 * 3。
这样p 就不是最小重复子串了,不符合我们定义的条件。
所以,**如果这个字符串s是由重复子串组成,那么最长相等前后缀不包含的子串是字符串s的最小重复子串**。
#### 必要性证明
以上是充分性证明,以下是必要性证明:
**如果 最长相等前后缀不包含的子串是字符串s的最小重复子串, 那么字符串s一定由重复子串组成吗**?
最长相等前后缀不包含的子串已经是字符串s的最小重复子串,那么字符串s一定由重复子串组成,这个不需要证明了。
关键是要要证明:最长相等前后缀不包含的子串什么时候才是字符串s的最小重复子串呢。
情况一, 最长相等前后缀不包含的子串的长度 比 字符串s的一半的长度还大,那一定不是字符串s的重复子串

--------------
情况二,最长相等前后缀不包含的子串的长度 可以被 字符串s的长度整除,如图:

步骤一:因为 这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,所以 s[0] 一定和 s[2]相同,s[1] 一定和 s[3]相同,即:,s[0]s[1]与s[2]s[3]相同 。
Expand All
@@ -121,39 +225,94 @@ KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一
所以字符串s,s[0]s[1]与s[2]s[3]相同, s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同。
正是因为 最长相等前后缀的规则,当一个字符串由重复子串组成的,最长相等前后缀不包含的子串就是最小重复子串。
可以推出,在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串。
即 s[0]s[1] 是最小重复子串
以上推导中,录友可能想,你怎么知道 s[0] 和 s[1] 就不相同呢? s[0] 为什么就不能使最小重复子串。
如果 s[0] 和 s[1] 也相同,同时 s[0]s[1]与s[2]s[3]相同,s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同,那么这个字符串就是有一个字符构成的字符串。
那么它的最长相同前后缀,就不是上图中的前后缀,而是这样的的前后缀:

录友可能再问,由一个字符组成的字符串,最长相等前后缀凭什么就是这样的。
有这种疑惑的录友,就是还不知道 最长相等前后缀 是怎么算的。
可以看这里:[KMP讲解](https://programmercarl.com/0028.%E5%AE%9E%E7%8E%B0strStr.html),再去回顾一下。
#### 简单推理
或者说,自己举个例子,`aaaaaa`,这个字符串,他的最长相等前后缀是什么?
这里再给出一个数学推导,就容易理解很多。
同上以上推导,最长相等前后缀不包含的子串的长度只要被 字符串s的长度整除,就是一定是最小重复子串。
假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。
----------------
因为字符串s的最长相同前后缀的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且 n - m = 1,(这里如果不懂,看上面的推理)
**情况三,最长相等前后缀不包含的子串的长度 不被 字符串s的长度整除得情况**,如图:
所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串。

next 数组记录的就是最长相同前后缀 [字符串:KMP算法精讲](https://programmercarl.com/0028.实现strStr.html) 这里介绍了什么是前缀,什么是后缀,什么又是最长相同前后缀), 如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。
最长相等前后缀的长度为:next[len - 1] + 1。(这里的next数组是以统一减一的方式计算的,因此需要+1,两种计算next数组的具体区别看这里:[字符串:KMP算法精讲](https://programmercarl.com/0028.实现strStr.html))
步骤一:因为 这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,t[2] 与 k[2]相同。
所以 s[0] 与 s[3]相同,s[1] 与 s[4]相同,s[2] 与s[5],即:,s[0]s[1]与s[2]s[3]相同 。
步骤二: 因为在同一个字符串位置,所以 t[3] 与 k[0]相同,t[4] 与 k[1]相同。
步骤三: 因为 这是相等的前缀和后缀,t[3] 与 k[3]相同 ,t[4]与k[5] 相同,所以,s[3]一定和s[6]相同,s[4]一定和s[7]相同,即:s[3]s[4] 与 s[6]s[7]相同。
以上推导,可以得出 s[0],s[1],s[2] 与 s[3],s[4],s[5] 相同,s[3]s[4] 与 s[6]s[7]相同。
那么 最长相等前后缀不包含的子串的长度 不被 字符串s的长度整除 ,就不是s的重复子串
-----------
充分条件:如果字符串s是由重复子串组成,那么 最长相等前后缀不包含的子串 一定是 s的最小重复子串。
必要条件:如果字符串s的最长相等前后缀不包含的子串 是 s最小重复子串,那么 s是由重复子串组成。
在必要条件,这个是 显而易见的,都已经假设 最长相等前后缀不包含的子串 是 s的最小重复子串了,那s必然是重复子串。
关键是需要证明, 字符串s的最长相等前后缀不包含的子串 什么时候才是 s最小重复子串。
同上我们证明了,当 最长相等前后缀不包含的子串的长度 可以被 字符串s的长度整除,那么不包含的子串 就是s的最小重复子串。
-------------
### 代码分析
next 数组记录的就是最长相同前后缀( [字符串:KMP算法精讲](https://programmercarl.com/0028.实现strStr.html)), 如果 `next[len - 1] != -1`,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。
最长相等前后缀的长度为:`next[len - 1] + 1`。(这里的next数组是以统一减一的方式计算的,因此需要+1,两种计算next数组的具体区别看这里:[字符串:KMP算法精讲](https://programmercarl.com/0028.实现strStr.html))
数组长度为:len。
如果len % ( len - (next[len - 1] + 1)) == 0 ,则说明数组的长度正好可以被 (数组长度-最长相等前后缀的长度) 整除 ,说明该字符串有重复的子字符串。
` len - (next[len - 1] + 1)` 是最长相等前后缀不包含的子串的长度。
**数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。**
如果`len % (len - (next[len - 1] + 1)) == 0` ,则说明数组的长度正好可以被 最长相等前后缀不包含的子串的长度 整除 ,说明该字符串有重复的子字符串。
### 打印数组
**强烈建议大家把next数组打印出来,看看next数组里的规律,有助于理解KMP算法**
如图:

next[len - 1] = 7,next[len - 1] + 1 = 8,8就是此时字符串asdfasdfasdf的最长相同前后缀的长度。
`next[len - 1] = 7`,`next[len - 1] + 1 = 8`,8就是此时字符串asdfasdfasdf的最长相同前后缀的长度。
`(len - (next[len - 1] + 1))` 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 为最长相同前后缀不包含的子串长度
(len - (next[len - 1] + 1)) 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 4正好可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。
4可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。
### 打码实现
C++代码如下:(这里使用了前缀表统一减一的实现方式)
Expand Down