最长回文子串问题是指,在给定的字符串中,找到最长的回文子串。回文串是指正着读和反着读都一样的字符串。
解法一:动态规划(Dynamic Programming)
这种方法比较直观,时间复杂度为 O(n²),空间复杂度为 O(n²)。
思路:
- 定义一个二维的布尔数组
dp[i][j],表示字符串从索引i到j是否是回文串。 - 如果
s[i] == s[j],并且dp[i+1][j-1]是回文串,那么dp[i][j]也是回文串。 - 动态规划的转移方程为:
dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1]。 - 初始条件:
- 单个字符总是回文串,所以
dp[i][i] = true。 - 两个连续相同的字符也是回文串,所以
dp[i][i+1] = (s[i] == s[i+1])。
- 单个字符总是回文串,所以
- 在填表的过程中,记录最长的回文子串的开始和长度。
代码实现:
def longest_palindrome(s: str) -> str:
if not s:
return ""
n = len(s)
# dp[i][j] 表示 s[i:j+1] 是否为回文串
dp = [[False] * n for _ in range(n)]
start = 0 # 记录最长回文串的起始位置
max_len = 1 # 记录最长回文串的长度
# 所有长度为 1 的子串都是回文串
for i in range(n):
dp[i][i] = True
# 处理长度为 2 的子串
for i in range(n - 1):
if s[i] == s[i + 1]:
dp[i][i + 1] = True
start = i
max_len = 2
# 处理长度大于 2 的子串
for length in range(3, n + 1): # length 是子串的长度
for i in range(n - length + 1):
j = i + length - 1 # 子串的结束位置
if s[i] == s[j] and dp[i + 1][j - 1]:
dp[i][j] = True
start = i
max_len = length
return s[start:start + max_len]
时间复杂度:
- 动态规划表的填充需要两个嵌套的循环,因此时间复杂度是 O(n²)。
空间复杂度:
- 需要一个二维的 DP 数组,空间复杂度为 O(n²)。
解法二:中心扩展法(Expand Around Center)
这种方法的时间复杂度为 O(n²),空间复杂度为 O(1)。
思路:
- 回文串的中心可以是一个字符,也可以是两个字符。
- 对于每一个字符,考虑它作为回文串的中心,向两边扩展,找到最长的回文串。
- 重复这个过程,找到最长的回文子串。
代码实现:
def longest_palindrome(s: str) -> str:
if not s:
return ""
def expand_around_center(left: int, right: int) -> str:
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
# 返回扩展后的最长回文子串
return s[left + 1:right]
longest = ""
for i in range(len(s)):
# 以 s[i] 为中心扩展
odd_palindrome = expand_around_center(i, i)
# 以 s[i] 和 s[i+1] 为中心扩展
even_palindrome = expand_around_center(i, i + 1)
# 更新最长回文子串
longest = max(longest, odd_palindrome, even_palindrome, key=len)
return longest
时间复杂度:
- 中心扩展的时间复杂度是 O(n²),每一个字符都尝试作为中心向两边扩展。
空间复杂度:
- 只需要常数级别的额外空间,因此空间复杂度为 O(1)。
解法三:Manacher's Algorithm(马拉车算法)
这是一个针对回文子串的线性时间算法,时间复杂度为 O(n)。
思路:
- 通过预处理将字符串转换为一个新的字符串,插入特殊字符(如
#)以避免处理奇偶长度的回文子串问题。 - 使用一个数组
p记录以每个字符为中心的回文子串的半径长度。 - 维护两个变量:
C:当前回文子串的中心。R:当前回文子串的右边界。
- 当遍历到一个新的位置时,利用之前已经计算的回文信息进行状态转移,减少重复计算。
代码实现:
def longest_palindrome(s: str) -> str:
# 特殊情况处理
if not s:
return ""
# 预处理字符串
T = '#'.join(f'^{s}$')
n = len(T)
p = [0] * n
C = R = 0
max_len = 0
center_index = 0
for i in range(1, n - 1):
mirror = 2 * C - i # i 关于 C 的对称位置
if R > i:
p[i] = min(R - i, p[mirror])
# 尝试扩展以 i 为中心的回文串
while T[i + p[i] + 1] == T[i - p[i] - 1]:
p[i] += 1
# 如果扩展后的回文串超过了 R,则更新 C 和 R
if i + p[i] > R:
C = i
R = i + p[i]
# 记录最长回文串的长度和中心位置
if p[i] > max_len:
max_len = p[i]
center_index = i
# 从处理后的字符串中找到最长回文串的起始位置
start = (center_index - max_len) // 2
return s[start:start + max_len]
时间复杂度:
- 这个算法的时间复杂度是 O(n),因为每个字符最多只被访问两次。
空间复杂度:
- 需要
O(n)的空间来存储预处理后的字符串和半径数组p。
总结:
- 动态规划:适用于理解简单,但时间和空间复杂度较高 (O(n²))。
- 中心扩展法:实现简单,适用于大多数情况,时间复杂度 O(n²),空间复杂度 O(1)。
- Manacher's Algorithm:时间复杂度为 O(n),是解决最长回文子串的最优算法,但实现较复杂。