给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
示例:
输入:"abab"
输出:True
题干很简单,也很容易理解,完整思路如《代码随想录》所示,归根结底可以总结出两种解法(暴力法不考虑)。
解法一:
如果一个字符串s是由一个子串重复多次构成,那么s+s拼接的字符串中,必定有一个s(不是拼接的那两个),如图。
那么题目就转化为了在s+s中寻找有没有s,利用python中的find函数就可以完成了,但是在s+s中寻找s总感觉奇奇怪怪的,既然是s+s那其中不本来就有s么,怎么判断是原始的s还是新组成的字符串中的s呢?那就可以将新组成的s+s第一个字符和最后一个字符删除,这样就不存在两端的s了。得到完整代码:
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
n = len(s)
if n <= 1:
return False
ss = s[1:] + s[:-1]
return ss.find(s) != -1
先计算字符串s的长度,如果长度小于等于1,那字符串最多只有一个字符,也不用考虑重复不重复的问题了,直接return到False。定义新的字符串ss为除s第一个元素的字符串与除s最后一个字符串的俩字符串拼接。
return到ss.find(s) != -1,find函数会返回s在ss中首次出现的位置索引,所以如果位置所以不等于-1的话则代表ss中存在s,那就return到True,反之则return到Flase,如果不好理解,就直接加个if条件吧。
解法二:
利用find的方法虽然代码简单,但是时间复杂度高。所以出现了解法二,时间复杂度低,但可能不是那么容易理解。
解法二是利用KMP算法进行求解,具体的关于KMP算法可以自行去学习,这里根据完整代码进行分析:
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
if len(s) == 0:
return False
nxt = [0] * len(s)
self.getNext(nxt, s)
if nxt[-1] != -1 and len(s) % (len(s) - (nxt[-1] + 1)) == 0:
return True
return False
def getNext(self, nxt, s):
nxt[0] = -1
j = -1
for i in range(1, len(s)):
while j >= 0 and s[i] != s[j+1]:
j = nxt[j]
if s[i] == s[j+1]:
j += 1
nxt[i] = j
return nxt
判断字符串s长度是不是等于0,接着定义了一个新的数组nxt,nxt是长度和s一样的空数组。然后利用一个自定义的函数getNext更新了nxt数组,接着对nxt数组进行了一个判定,就直接得到了最终的结果了,这是整个流程。
首先,我们来分析一下这个自定义函数getNext,在分析之前,我先简单讲一下这个解法的原理,如果字符串s是由它的子串重复构成的,那么一定存在两种特征,一是最小子串的长度是能被原始子串s长度整除的(这个很容易理解,最小子串长度如果是3的话,那你重复构成的s长度肯定是3的倍数吧);二是在s前后删除最小子串后得到的两个字符串应该是一样的,如图:
接着,我们来看自定义函数getNext,首先将数组第一个元素定义为-1,又把-1赋值给j,光看这一步看不出什么意思,那么我们结合后面的代码分析。
以上图的abababab为例,第一次循环i=1,因为j=-1,while不匹配;因为s[1]=b和s[j+1]=s[0]=a不相等,if不匹配。那么将nxt数组位置索引1赋值为-1,当前nxt为:
第二次循环i=2,因为j=-1,while不匹配;因为s[2]=a和s[j+1]=s[0]=a相等,所以进入if,j+1=0。将0赋值给nxt[2],当前nxt为:
第三次循环i=3,因为j=0,s[3]=b,s[j+1]=s[1]=b,两者相等,while不匹配,进入if,j=1,n[3]=1,当前nxt为:
继续循环,直到最后得到nxt为:
结合循环进行分析,
if s[i] == s[j+1]:
j += 1
nxt[i] = j
如果一直循环都没有和第一个字符重复的字符存在,那么nxt[i]就一直为-1。直到存在一个字符和第一个字符相同,j开始变化+1,但光有第一个字符相等也不行,我们还要看后续字符是不是和前面那个子串一样
while j >= 0 and s[i] != s[j+1]:
j = nxt[j]
当存在有和第一个字符一样的时候,如果后续开始和前面字符串重复了,那么恭喜你,j可以一直叠加1。那如果从重复值开始后的相对位置的s[i]不等于前面字符串相对位置的字符了呢?那就证明还没找到最小子串,即abac,虽然a重复了,但后续的c和b又不一样了,那么这种情况下就把nxt[j],即对应前面字符串相对位置的数赋值给j,此时肯定是-1。
直到所有循环结束,如果是由子串重复组成的话,那么nxt的最后一个元素值肯定为len(s)减去子串长度再-1。因此进入 if nxt[-1] != -1 and len(s) % (len(s) - (nxt[-1] + 1)) == 0:判断。
首先判断nxt[-1] != -1意味着如果nxt最后一个数为-1,那肯定不是由子串重复构成,如果不为-1,那么可能有两种情况,一是由子串重复构成,二是由子串出现且后续字符和子串有部分相等,例如ababac,虽然不是由重复子串构成,但存在一次重复,则nxt最后一个也不会为0。
接着就需要判断len(s) % (len(s) - (nxt[-1] + 1)) == 0。根据之前讲过的条件,去整除判断是否由子串重复构成。