题目分析:要求计算给定字符串中最长的回文字符串。
给你一个字符串 s
,找到 s
中最长的 回文子串.
示例 1:
输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。
示例 2:输入:s = "cbbd" 输出:"bb"
提示:
1 <= s.length <= 1000
s
仅由数字和英文字母组成
主要思路:
- 可以使用动态规划或中心扩展法来解决。
- 动态规划通过构建一个二维矩阵来记录子串是否为回文,并根据状态转移方程进行计算。
- 中心扩展法从字符串的每个位置开始,向两边扩展,判断是否为回文。
时间复杂度:
- 动态规划的时间复杂度为 O(n^2),其中 n 是字符串的长度。
- 中心扩展法的时间复杂度也为 O(n^2)。
空间复杂度:
- 动态规划需要使用一个二维矩阵,空间复杂度为 O(n^2)。
- 中心扩展法使用的额外空间较少,通常为 O(1)。
方法一:动态规划
以下是使用动态规划解决该问题的 Python 代码示例:
def longest_palindrome(s):
# 获取字符串的长度
n = len(s)
# 创建一个二维数组 dp,用于存储子串是否为回文的状态
dp = [[False] * n for _ in range(n)]
# 初始化答案字符串为空
ans = ""
# 初始化边界情况:单个字符本身是回文
for i in range(n):
dp[i][i] = True
# 动态规划计算
for l in range(2, n + 1): # 从长度为 2 的子串开始逐步计算
for i in range(n - l + 1): #在字符串中从索引 0 开始,以步长为 1 ,遍历到索引为 n - l + 1 的位置。
j = i + l - 1 #计算当前子串的结束位置
if j >= n: # 如果超出字符串长度,结束当前循环
break
# 判断当前子串是否为回文
dp[i][j] = (dp[i + 1][j - 1] and s[i] == s[j])
if dp[i][j] and l + 1 > len(ans): # 如果是回文且长度更长,更新答案
ans = s[i:j + 1]
return ans # 返回最长回文子串
class Solution:
def longestPalindrome(self, s: str) -> str:
dp = [[False] * len(s) for _ in range(len(s))]
maxlenth = 0
left = 0
right = 0
for i in range(len(s) - 1, -1, -1):
for j in range(i, len(s)):
if s[j] == s[i]:
if j - i <= 1 or dp[i + 1][j - 1]:
dp[i][j] = True
if dp[i][j] and j - i + 1 > maxlenth:
maxlenth = j - i + 1
left = i
right = j
return s[left : right + 1]
第一段代码:
- 使用了一个二维数组
dp
来记录子串是否为回文。 - 从较小的子串长度开始逐步计算到较大长度。
- 通过判断当前子串的两端字符和内部子串是否回文来确定当前子串是否回文。
- 直接在计算过程中更新最长回文子串。
第二段代码:
- 同样使用二维数组
dp
。 - 循环的顺序略有不同,是从后向前遍历起始位置。
- 在判断回文时的逻辑与第一段类似,但在更新最长回文子串时,根据长度和具体位置进行更新。
二者在核心思路上是一致的,都是基于动态规划的方法,主要差异在于一些细节的处理和循环结构上的不同。
注:dp = [[False] * len(s) for _ in range(len(s))],这里做个解释。
理解range(len(s)):
len(s)计算字符串s的长度。
range(len(s))生成一个从0到len(s)-1的整数序列,这个序列将用作外层循环的索引。
理解外层列表推导式:
外层列表推导式由for _ in range(len(s))构成,它迭代range生成的每个索引。
理解内层列表推导式:
内层列表推导式[False] * len(s)在每次外层循环迭代时执行。
它创建了一个长度等于len(s)的列表,列表中的每个元素都是False。
嵌套执行过程:
对于range(len(s))中的每个索引(假设len(s)为5),外层循环执行一次。
每次外层循环迭代时,内层列表推导式创建一个新的布尔列表,长度为5(即len(s)),并填充为False。
这个新创建的布尔列表成为外层列表的一个元素,即二维数组的一行。
最终结果:
经过len(s)次迭代,外层循环创建了一个包含len(s)个布尔列表的列表,每个布尔列表的长度也是len(s)。
结果是一个二维布尔数组,其维度是len(s)×len(s)。
举例说明:
假设s = "abc",那么len(s)是3。执行过程如下:
第一次迭代:创建一个长度为3的布尔列表[False, False, False]。
第二次迭代:再创建一个长度为3的布尔列表[False, False, False]。
第三次迭代:再创建一个长度为3的布尔列表[False, False, False]。
最终结果是:
[
[False, False, False], # 第一次迭代的结果
[False, False, False], # 第二次迭代的结果
[False, False, False] # 第三次迭代的结果
]
这个二维数组的每个位置dp[i][j]可以用于存储从索引i到索引j的子问题的解或状态。希望这个逐步解释有助于你理解列表推导式的嵌套顺序和创建二维数组的过程。
方法二:中心扩展算法
def longest_palindrome(s):
n = len(s)
ans = ""
for i in range(n):
for j in range(i, n):
if j - i == 1:
dp[i][j] = True
elif s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1]
else:
dp[i][j] = False
if dp[i][j] and j - i + 1 > len(ans):
ans = s[i:j + 1]
return ans
中心扩展法和动态规划法的时间复杂度都是 O(N^2),但中心扩展法的空间复杂度是 O(1),而动态规划法的空间复杂度是 O(N)。因此,从时间复杂度和空间复杂度的角度来看,中心扩展法更优。
方法三:Manacher 算法
还有一个复杂度为 O(n) 的 Manacher 算法。然而本算法十分复杂,供有兴趣的同学挑战自己。
为了表述方便,我们定义一个新概念臂长,表示中心扩展算法向外扩展的长度。如果一个位置的最大回文字符串长度为 2 * length + 1 ,其臂长为 length。
下面的讨论只涉及长度为奇数的回文字符串。长度为偶数的回文字符串我们将会在最后与长度为奇数的情况统一起来。
思路与算法
在中心扩展算法的过程中,我们能够得出每个位置的臂长。那么当我们要得出以下一个位置 i 的臂长时,能不能利用之前得到的信息呢?
答案是肯定的。具体来说,如果位置 j 的臂长为 length,并且有 j + length > i,如下图所示:
当在位置 i 开始进行中心拓展时,我们可以先找到 i 关于 j 的对称点 2 * j - i。那么如果点 2 * j - i 的臂长等于 n,我们就可以知道,点 i 的臂长至少为 min(j + length - i, n)。那么我们就可以直接跳过 i 到 i + min(j + length - i, n) 这部分,从 i + min(j + length - i, n) + 1 开始拓展。
我们只需要在中心扩展法的过程中记录右臂在最右边的回文字符串,将其中心作为 j,在计算过程中就能最大限度地避免重复计算。
那么现在还有一个问题:如何处理长度为偶数的回文字符串呢?
我们可以通过一个特别的操作将奇偶数的情况统一起来:我们向字符串的头尾以及每两个字符中间添加一个特殊字符 #,比如字符串 aaba 处理后会变成 #a#a#b#a#。那么原先长度为偶数的回文字符串 aa 会变成长度为奇数的回文字符串 #a#a#,而长度为奇数的回文字符串 aba 会变成长度仍然为奇数的回文字符串 #a#b#a#,我们就不需要再考虑长度为偶数的回文字符串了。
注意这里的特殊字符不需要是没有出现过的字母,我们可以使用任何一个字符来作为这个特殊字符。这是因为,当我们只考虑长度为奇数的回文字符串时,每次我们比较的两个字符奇偶性一定是相同的,所以原来字符串中的字符不会与插入的特殊字符互相比较,不会因此产生问题。
class Solution:
def expand(self, s, left, right):
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return (right - left - 2) // 2
def longestPalindrome(self, s: str) -> str:
end, start = -1, 0
s = '#' + '#'.join(list(s)) + '#'
arm_len = []
right = -1
j = -1
for i in range(len(s)):
if right >= i:
i_sym = 2 * j - i
min_arm_len = min(arm_len[i_sym], right - i)
cur_arm_len = self.expand(s, i - min_arm_len, i + min_arm_len)
else:
cur_arm_len = self.expand(s, i, i)
arm_len.append(cur_arm_len)
if i + cur_arm_len > right:
j = i
right = i + cur_arm_len
if 2 * cur_arm_len + 1 > end - start:
start = i - cur_arm_len
end = i + cur_arm_len
return s[start+1:end+1:2]
时间复杂度:O(n),其中 nnn 是字符串的长度。由于对于每个位置,扩展要么从当前的最右侧臂长 right 开始,要么只会进行一步,而 right 最多向前走 O(n)步,因此算法的复杂度为 O(n)。
空间复杂度:O(n),我们需要 O(n)的空间记录每个位置的臂长。