题目描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
解决思路1
回文字符串,就是顺序读取和逆序读取的结果是一样的,比如“上海自来水来自海上”,我们在判断回文字符串的时候,可以认为回文字符串都是有一个中心的,比如“上海自来水来自海上”的中心就是“水”,以“水”为中心,向两边拓展,每个对应的字符都是一样的,则我们认为这是一个回文字符串,这就是中心拓展思想。
但是我们应该还考虑一种情况,回文字符串长度为偶数的时候,比如“1221”,这个字符串的中心在“22”中间,依然可以用中心拓展思想来做,但是我们如何在代码里表示它的中心呢?我们在这里试着在字符串中间插入“#”字符,原来的字符串就变为“1#2#2#1”,那么这个回文字符串的中心就是“#”,对上面的字符串也进行字符中间插入“#”,那么“上#海#自#来#水#来#自#海#上”的中心依然是“水”,那么就很好的解决了字符串长度为奇数,或者偶数的问题。
我们观察上面的两个回文字符串可以发现,在转换之后的字符串中,第0,2,4,6…个字符是原来字符串中的字符,我们插入的“#”字符均在奇数位置上,那么我们就在遍历寻找中心的时候,当索引i指向偶数位置的时候,start = end = i//2,当索引i指向奇数位置的时候start = (i-1) // 2,end = (i+1)//2。具体代码如下:
#中心拓展算法
#时间复杂度O(n^2)
class Solution:
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
1#2#3#2#1
"""
str_ = ""
for i in range(2*len(s)-1):
if i%2 == 0: #索引i指向偶数位置
start = end = i//2
while start>=0 and end<len(s) and s[start]==s[end]:
start-=1
end+=1
else: #索引i指向奇数位置
start = (i-1) // 2
end = (i+1) //2
while start>=0 and end<len(s) and s[start]==s[end]:
start-=1
end+=1
if len(str_)<=(end-start-1): #更新最长字符串
str_ = s[start+1:end]
return str_
结果 1296 ms
解决思路2
我们在判断回文字符串的时候,正常的情况下会这么判断:如i、j分别指向两个字符,如果字符i等于字符j,那么只要s[i+1,j-1]是回文字符串,那么s[i:j]也是回文字符串。这个就是动态规划的思想,我们在求解一个复杂问题的时候,可以利用以前的结果来缩短我们所需要的计算量。我们设置一个len(s)*len(s)的二维矩阵tmp,如果tmp[i][j]为0,则表示s[i:j]不是回文字符串,tmp[i][j]为1时,则表示s[i:j]为回文字符串,那么我们的思想可以简单归述为下面这个公式:
下面附上我们的实现的代码:
#动态规划思想
#时间复杂度O(n^2)
class Solution:
def longestPalindrome(self,s):
"""
:type s:str
:rtype: str
"""
str_ = ''
s = list(s)
tmp = [[0]*len(s) for i in range(len(s))] #生成二维数组
for len_ in range(1,len(s)+1): #长度依次从1开始遍历
for i in range(len(s)):
j = len_+i-1
if j<len(s):
if i==j:
tmp[i][j] = 1
if i+1 == j and s[i] == s[j]:
tmp[i][j] = 1
if j>i+1 and s[i] == s[j] and tmp[i+1][j-1]==1:
tmp[i][j] = 1
if tmp[i][j] == 1 and len(str_)<(j-i+1):
str_ = ''.join(s[i:j+1])
return str_
结果 3836ms (偶尔会发生超出时间限制)
解决思路3
在处理最长回文子串的时候,有个线性的算法–Manacher算法,他可以将n^2的复杂度降到线性,大大缩短了程序计算的时间。
由于回文串的长度可奇可偶,比如"bob"是奇数形式的回文,"noon"就是偶数形式的回文,马拉车算法的第一步是预处理,做法是在每一个字符的左右都加上一个特殊字符,比如加上’#’,那么
bob --> #b#o#b#
noon --> #n#o#o#n#
这样做的好处是不论原字符串是奇数还是偶数个,处理之后得到的字符串的个数都是奇数个,这样就不用分情况讨论了,而可以一起搞定。接下来我们还需要和处理后的字符串t等长的数组p,其中p[i]表示以t[i]字符为中心的回文子串的半径,若p[i] = 1,则该回文子串就是t[i]本身,那么我们来看一个简单的例子:
#1 # 2 # 2 # 1 # 2 # 2 #
1 2 1 2 5 2 1 6 1 2 3 2 1
为啥我们关心回文子串的半径呢?看上面那个例子,以中间的 ‘1’ 为中心的回文子串 “#2#2#1#2#2#” 的半径是6,而未添加#号的回文子串为 “22122”,长度是5,为半径减1。这是个普遍的规律么?我们再看看之前的那个 “#b#o#b#”,我们很容易看出来以中间的 ‘o’ 为中心的回文串的半径是4,而 “bob"的长度是3,符合规律。再来看偶数个的情况"noon”,添加#号后的回文串为 “#n#o#o#n#”,以最中间的 ‘#’ 为中心的回文串的半径是5,而 “noon” 的长度是4,完美符合规律。所以我们只要找到了最大的半径,就知道最长的回文子串的字符个数了。只知道长度无法定位子串,我们还需要知道子串的起始位置。
我们还是先来看中间的 ‘1’ 在字符串 “#1#2#2#1#2#2#” 中的位置是7,而半径是6,貌似7-6=1,刚好就是回文子串 “22122” 在原串 “122122” 中的起始位置1。那么我们再来验证下 “bob”,“o” 在 “#b#o#b#” 中的位置是3,但是半径是4,这一减成负的了,肯定不对。所以我们应该至少把中心位置向后移动一位,才能为0啊,那么我们就需要在前面增加一个字符,这个字符不能是#号,也不能是s中可能出现的字符,所以我们暂且就用美元号$。这样都不相同的话就不会改变p值了,那么末尾也要增加一个特殊字符,我们设置为“`”。那此时 “o” 在 “$#b#o#b#`” 中的位置是4,半径是4,一减就是0了,貌似没啥问题。我们再来验证一下那个数字串,中间的 ‘1’ 在字符串 “$#1#2#2#1#2#2#`” 中的位置是8,而半径是6,这一减就是2了,而我们需要的是1,所以我们要除以2。之前的 “bob” 因为相减已经是0了,除以2还是0,没有问题。再来验证一下 “noon”,中间的 ‘#’ 在字符串 “$#n#o#o#n#`” 中的位置是5,半径也是5,相减并除以2还是0,完美。可以任意试试其他的例子,都是符合这个规律的,最长子串的长度是半径减1,起始位置是中间位置减去半径再除以2。
那么我们的问题就转移到,如何求的数组p,只要求得一个正确的数组p,那么我们就可以正确的找到一个最长回文子串。我们设立两个辅助参数,center和mx,mx是回文串能延伸到的最右端的位置,center是延伸到最右端位置的回文串的中心。
当我们求解p的时候,我们可能就想到了上面的中心拓展算法,我们可以用中心拓展算法依次求得每个字符作为中心时的半径。但是我们有没有比较简便的方法计算p呢?
我们考虑到在从左往右遍历字符时,i永远都在center的右侧,那我们考虑i
当i>mx的时候,我们发现直接使用中心拓展算法,然后更新center和mx即可。 我们实现的代码如下: 结果 120ms GitHub地址:
当i#manacher算法
#时间复杂度O(n)
class Solution:
def longestPalindrome(self,s):
if len(s) <= 1:
return s
# 每个字符之间插入 #
ss = '$#' + '#'.join([x for x in s]) + '#`'
p = [1] * len(ss)
center = 0
mx = 0
max_str = ''
for i in range(1, len(p)-1):
if i < mx:
j = 2 * center - i # i 关于 center 的对称点
p[i] = min(p[j],mx-i)
# 尝试继续向两边扩展,更新 p[i]
while ss[i - p[i] ] == ss[i + p[i] ]: # 不必判断是否溢出,因为首位均有特殊字符,肯定会退出
p[i] += 1
# 更新中心
if i + p[i] - 1 > mx:
mx = i + p[i] - 1
center = i
# 更新最长串
if 2 * p[i]-1 > len(max_str):
max_str = ss[i - p[i]+1 : i + p[i]]
return max_str.replace('#', '')
https://github.com/wanghaoying/leetcode
参考博客:
Manacher’s Algorithm 马拉车算法:https://www.cnblogs.com/grandyang/p/4475985.html