找出字符串的最短重复子串
在研究字符串时,我们经常遇到需要判断某个字符串是否可以通过一个较短的子串重复拼接构成的问题。本问题的目标是找到这样一个最短的子串,如果字符串不能通过重复构成,则返回空字符串。
问题背景与描述
假设我们有一个字符串 inp,需要判断是否存在一个较短的子串 s,使得 inp 是通过 s 的重复拼接得到的。例如:
- 输入
"abcabcabcabc"时,结果应为"abc",因为它是通过"abc"重复 4 次得到的。 - 输入
"abababab"时,结果应为"ab"。 - 输入
"ab"时,没有符合要求的子串,结果为空字符串""。
直观来看,我们可以通过逐步截取子串并判断是否可以完整拼接构成原字符串的方式解决问题。然而,这种方法的时间复杂度较高,尤其是在字符串长度较大时,效率不足。因此,我们需要设计更高效的算法。
解题思路
我们利用 字符串拼接特性 和 KMP 的部分匹配思想 来解决问题,通过以下几个步骤完成:
1. 双倍字符串构造
如果一个字符串可以通过某个子串 s 重复拼接得到,那么将该字符串拼接一次形成双倍字符串,去掉首尾字符后,仍然能在新字符串中找到原字符串。这是因为:
- 双倍字符串中,子串
s的重复部分会在新字符串中自然出现。 - 若字符串不能通过某个子串拼接得到,则在双倍字符串去掉首尾字符后,原字符串的模式将无法匹配。
例如:
- 对于字符串
abcabc:- 双倍拼接为
abcabcabcabc。 - 去掉首尾字符得到
bcabcabcab,原字符串abcabc可在新字符串中找到。
- 双倍拼接为
- 对于字符串
ab:- 双倍拼接为
abab。 - 去掉首尾字符得到
ba,原字符串无法匹配。
- 双倍拼接为
2. 查找原字符串
我们通过 Python 的内置方法 find 查找原字符串在修改后的双倍字符串中的第一次出现位置。
- 如果能够找到,则证明字符串
inp是由某个子串重复拼接得到。 - 如果找不到,则说明字符串不能通过子串拼接构成。
3. 提取最短子串
一旦找到匹配,我们利用返回的索引 pos 推导出最短子串的长度:
- 假设
pos为原字符串在新字符串中第一次出现的位置,则最短子串长度为pos + 1。
这是因为在双倍字符串中,匹配的开始位置表明子串重复的最小周期。
4. 特殊情况
- 如果字符串长度为 1,则无论如何都无法通过重复拼接构成,应直接返回空字符串。
- 如果输入为空字符串,则同样返回空。
算法实现
以下是基于上述思路实现的 Python 函数:
def find_shortest_repeated_substring(inp: str) -> str:
n = len(inp)
doubled_inp = inp + inp # 拼接字符串
modified_inp = doubled_inp[1:-1] # 去掉首尾字符
# 在去掉首尾字符的字符串中查找原字符串
pos = modified_inp.find(inp)
if pos != -1: # 找到匹配
return inp[:pos + 1] # 最短子串长度为 pos + 1
return "" # 没有匹配
示例与分析
示例 1: inp = "abcabcabcabc"
- 双倍拼接:
"abcabcabcabcabcabcabcabc"。 - 去掉首尾:
"bcabcabcabcabcabcabcab"。 - 查找原字符串:
find("abcabcabcabc")返回索引3。 - 最短子串:
inp[:3 + 1] = "abc"。
示例 2: inp = "aaa"
- 双倍拼接:
"aaaaaa"。 - 去掉首尾:
"aaaaa"。 - 查找原字符串:
find("aaa")返回索引0。 - 最短子串:
inp[:0 + 1] = "a"。
示例 3: inp = "ab"
- 双倍拼接:
"abab"。 - 去掉首尾:
"ba"。 - 查找原字符串:无法匹配,返回
-1。 - 结果:空字符串
""。
时间与空间复杂度分析
时间复杂度
- 拼接和截取字符串的操作为 ( O(n) )。
find方法的复杂度为 ( O(n) )(基于 KMP 算法)。- 总时间复杂度:( O(n) )。
空间复杂度
- 拼接字符串和截取字符串占用 ( O(n) ) 空间。
- 总空间复杂度:( O(n) )。
总结
通过字符串拼接的方式,我们可以高效地判断字符串是否可以由某个子串重复拼接构成,同时提取最短的子串。相比于暴力法,本算法利用字符串的周期性特性,避免了逐个子串的枚举判断,提高了效率。这种方法非常适合处理大规模字符串匹配问题,在实际应用中具有重要意义。