链接
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba"也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
这题应该算是面试中的经典题目了,题目描述很简单就是要找字符串中的最长的回文数
找到所有的回文字串,然后找到最长的
时间复杂度:O(n^3)
这一定是超时的
现在我们从回数的特点入手。
假若一个字符串是一个回数,那么该字符串内部一定还存在更多的回数。例如,abbbcbbba是一个回数,那么bbbcbbb一定是一个回数,那么bcb也是回数,最后到b。同理,bccb是一个回数,那么cc也是一个回数。因此可以看出,假设当前一个字符串是回数,那么加上两侧的字符可能还是回数。假设当前一个字符串不是回数,那么加上右侧的字符可能构成一个回数。
因此,假设当前得到的回数的最大长度为n,我们可以判断n+1或者n+2是不是回数。
为什么这么判断呢?下面给出证明。
我们假设有一个字符串xxxxxxxxabaxxxxxxs,其中x代表任意字符。
假设此时指针指向s,而已知最大回数子字符串的长度为3。我们只需要判断xxxs以及xxxxs是不是回数。无需判断xxs乃至更近是因为它们的长度必然无法超过当前的最大长度。而无需判断xxxxxs乃至更远是因为假如xxxxxs是回数,那么xxxx一定是回数,则当前的最大长度为4而不是3,与题设不符。所以只需判断两种情况。
这里就充分利用了回数的性质,省去了很多无效的遍历
class Solution:
# 判断是不是回文数
def isPalindrome(self, s, start, end):
if start < 0:
return False
while start < end:
if s[start] != s[end]:
return False
start += 1
end -= 1
return True
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
res = ""
curr_len = 0
for i in range(len(s)):
if self.isPalindrome(s, i - curr_len - 1, i):
res = s[i - curr_len - 1: i + 1]
curr_len += 2
elif self.isPalindrome(s, i - curr_len, i):
res = s[i - curr_len : i + 1]
curr_len += 1
return res
时间复杂度:O(n^2)
通过从中间向两边查找,判断是否是回文数
时间复杂度:O(n^2)
class Solution2:
"""
通过从中间向两边查找,判断是否是回文数,时间复杂度:O(n^2)
"""
start, end = 0, 0
def extendsPalindrome(self, s, j, k,):
while j >= 0 and k < len(s) and s[j] == s[k]:
j -= 1
k += 1
return k - j - 1
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
length = len(s)
if length < 2:
return s
for i in range(length):
# 如果是奇数长度
len1 = self.extendsPalindrome(s, i, i)
# 如果是偶数长度
len2 = self.extendsPalindrome(s, i, i + 1)
max_len = max(len1, len2)
if max_len > self.end - self.start:
self.start = i - (max_len - 1) // 2
self.end = i + (max_len) // 2
return s[self.start : self.end + 1]
以上都是时间复杂度在 O(n^2)解决问题的,但是其实求字符串的最长回文数还有O(n)的方法,叫Manacher’s algorithm(马拉车算法)
首先我们将输入字符串S
通过在每个字符间插入#
来转换成T
,例如如下:
S = “abaaba”, T = “#a#b#a#a#b#a#”
为了找到最长的回文子串,我们需要在每个 Ti T i 周围扩展,使得 Ti−d....Ti+d T i − d . . . . T i + d 形成回文。你应该马上明白d是以 Ti T i 为中心的回文的长度。
我们将中间结果存储在数组 P P 中,其中 P[i] P [ i ] 等于在 Ti T i 处的回文中心的长度。最长的回文子串将成为 P P 中的最大元素。
使用上面的例子,我们填充 P P 如下(从左到右):
T = # a # b # a # a # b # a #
P = 0 1 0 3 0 1 6 1 0 3 0 1 0
通过这个表我们可以看见 P6=6 P 6 = 6 ,所以我们可以知道最长的回文数是abaaba
您是否注意到通过在字母之间插入特殊字符(#),这两个长度均为偶数的回文是否被优雅地处理?(请注意:这是为了更容易地演示这个想法,并不一定需要对算法进行编码。)
现在,想象你在回文中心“abaaba”绘制一条想象的垂直线。你有没有注意到 P P 中的数字是围绕这个中心对称的?不仅如此,请尝试使用另一个回文“aba”,这些数字也反映了类似的对称性。这是巧合吗?肯定不是。这仅仅是一个条件,但无论如何,我们有很大的进步,因为我们可以消除 P[i] P [ i ] 的重新计算部分。
让我们继续讨论一个稍微复杂的例子,其中有更多的重叠回文,其中S =“babcbabcbaccba”
。
上图显示 T T 由S =“babcbabcbaccba”
转化而来。假设你达到了表 P P 部分完成的状态。垂直的实线表示回文“abcbabcba”
的中心 (C) ( C ) 。两条虚线垂直线分别表示其左 (L) ( L ) 和右 (R) ( R ) 边缘。你现在处于索引 i i 处,其围绕 C C 的镜像索引是 i′ i ′ 。你如何有效地计算 P[i] P [ i ] ?
假设我们已经到达指数 i=13 i = 13 ,并且我们需要计算 P[13] P [ 13 ] (由问号?表示)。我们首先看一下它在回文中心 C C 周围的镜像索引 i′ i ′ ,索引 i′=9 i ′ = 9 。
面的两条绿色实线表示以i和i’为中心的两个回文序列覆盖的区域。我们看一下 C C 周围的镜像索引,它是索引 i′ i ′ 。 P[i′]=P[9]=1 P [ i ′ ] = P [ 9 ] = 1 .由于回文在其中心附近具有对称性,因此 P[i] P [ i ] 必须也是1。
正如你在上面看到的那样, P[i]=P[i′]=1 P [ i ] = P [ i ′ ] = 1 是非常明显的,由于回文中心周围的对称性,这一定是正确的。事实上, C C 之后的所有三个元素都遵循对称性(即 P[12]=P[10]=0,P[13]=P[9]=1,P[14]=P[8]=0 P [ 12 ] = P [ 10 ] = 0 , P [ 13 ] = P [ 9 ] = 1 , P [ 14 ] = P [ 8 ] = 0 )。
现在我们处于索引 i=15 i = 15 ,其 C C 上的镜像索引是 i′=7 i ′ = 7 。那么 [15]=P[7]=7? [ 15 ] = P [ 7 ] = 7 ?
现在我们处于索引 i=15 i = 15 处。 P[i] P [ i ] 的值是什么?如果我们遵循对称性, P[i] P [ i ] 的值应该与 P[i′]=7 P [ i ′ ] = 7 相同,但这是错误的。如果我们在 T15 T 15 围绕中心展开,它形成回文“a#b#c#b#a”
,这实际上是比它的对称的 T7 T 7 显示短。为什么?
在索引 i i 和 i′ i ′ 中央围绕着彩色线条。绿色实线表示由于 C C 周围的对称性而必须匹配两侧的区域。红色实线表示可能不匹配两侧的区域。虚线绿色线条显示穿过中心的区域。
很显然,由两条实线表示的区域中的两个子串必须完全匹配。中心区域(用绿色虚线表示)也必须是对称的。注意 P[i′] P [ i ′ ] 是7,并且它一直扩展到回文的左边缘 (L) ( L ) (用红色实线表示),它不再落在回文的对称属性之下。我们所知道的是 P[i]≥5 P [ i ] ≥ 5 ,并且为了找到P [i]的实际值,我们必须通过扩展右边缘 (R) ( R ) 来进行字符匹配。在这种情况下,由于 P[21]≠P[1] P [ 21 ] ≠ P [ 1 ] ,又因为 P[15] P [ 15 ] 和 P[7] P [ 7 ] 是对应的, P[21] P [ 21 ] 和 P[1] P [ 1 ] 分别在他们的两侧,我们得出结论 P[i]=5 P [ i ] = 5 。
总结一下这个算法的关键部分如下:
如果 P [i']≤R-i,
则 P [i]←P [i']
否则 P [i]≥P[i']。(我们必须扩展到右边缘(R)以找到P [i]。
看看它有多优雅?如果你能够充分掌握上述总结,那么你已经获得了这个算法的本质,这也是最难的部分。
最后一部分是确定我们应该在何时将C的位置与R一起移动到右侧,这很容易:
如果以i为中心的回文确实扩展到R,我们将C更新为i(这个新回文的中心),并将R延伸到新回文的右边。
在每一步中,都有两种可能性。如果 P[i]≤R−i P [ i ] ≤ R − i ,我们将 P[i] P [ i ] 设置为 P[i′] P [ i ′ ] ,这只需要一步。否则,我们试图通过从右边缘R开始将回文中心改为i。扩展R(内部while循环)总共最多需要N个步骤,并且定位和测试每个中心总共需要N步太。因此,该算法确保在至多2 * N步完成,给出线性时间解决方案如下:
class Solution3:
"""
Manacher's algorithm Time complexity:O(n)
"""
def preprocess(self, s):
length = len(s)
if length == 0:
return "^$"
ret = "^"
for i in range(length):
ret += "#" + s[i]
ret += "#$"
return ret
def longestPalindrome(self, s):
if len(s) < 2:
return s
t = self.preprocess(s)
n = len(t)
p = [0 for i in range(n)]
C, R = 0, 0
for i in range(1, n - 1):
# 等于i' = C - (i - C)
i_mirror = 2 * C - i
# 最核心的算法思想
if R > i:
p[i] = min(R - i, p[i_mirror])
else:
p[i] = 0
while t[i + 1 + p[i]] == t[i - 1 - p[i]]:
p[i] += 1
# 如果以i为中心的回文串扩展超过R,基于扩展的回文串扩展调整中心位置
if i + p[i] > R:
C, R = i, i + p[i]
max_len = 0
center_index = 0
for i in range(1, n - 1):
if p[i] > max_len:
max_len = p[i]
center_index = i
start = (center_index - max_len) // 2
end = start + max_len
# print(start, max_len)
return s[start:end]
leetcode solution
Manacher’s algorithm
segmentfault