题目链接:5. 最长回文子串
目录
- [1. 题目理解](#1. 题目理解)
- [2. 解题思路与方法讨论](#2. 解题思路与方法讨论)
- 方法一:中心扩展法 (Expand Around Center)
- 方法二:动态规划 (Dynamic Programming)
- [方法三:Manacher 算法 (马拉车算法)](#方法三:Manacher 算法 (马拉车算法))
- [3. 代码实现 (中心扩展法)](#3. 代码实现 (中心扩展法))
- [4. 代码实现 (动态规划法)](#4. 代码实现 (动态规划法))
- [5. 复杂度分析](#5. 复杂度分析)
- [6. 总结与建议](#6. 总结与建议)
1. 题目理解
题目目标 :
给定一个字符串 s,你需要找到它里面最长 的一个子串 ,这个子串必须是回文的。
关键概念:
- 子串 (Substring) :字符串中连续的一段字符。例如
"babad"中,"bab"是子串,但"bd"不是(因为不连续)。 - 回文 (Palindrome) :正着读和反着读都一样的字符串。例如
"aba","bb","a"都是回文。
示例分析:
- 输入
"babad"-> 输出"bab"或"aba"。 - 输入
"cbbd"-> 输出"bb"。
数据范围 :
字符串长度 1 <= s.length <= 1000。这意味着 \(O(N^2)\) 的算法是可以接受的(\(1000^2 = 1,000,000\) 次运算,在现代计算机上很快),但 \(O(N^3)\) 可能会超时。
2. 解题思路与方法讨论
这道题是经典的字符串算法题,主要有三种解法,难度和效率依次递增。
方法一:中心扩展法 (Expand Around Center)
核心思想 :
回文串就像照镜子,它是关于"中心"对称的。
- 如果回文串长度是奇数 (如
"aba"),中心就是中间那个字符('b')。 - 如果回文串长度是偶数 (如
"abba"),中心就是中间两个字符之间的空隙(两个'b'之间)。
算法步骤:
- 遍历字符串的每一个位置
i。 - 将
i视为奇数长度回文串的中心,向左右两边扩展,直到字符不相等。 - 将
i和i+1视为偶数长度回文串的中心,向左右两边扩展,直到字符不相等。 - 在扩展过程中,记录遇到的最长回文串的起始位置和长度。
优点:
- 空间复杂度低,只需要 \(O(1)\) 的额外空间。
- 代码逻辑直观,容易编写。
- 实际运行效率通常比动态规划高。
缺点:
- 时间复杂度为 \(O(N^2)\)。
方法二:动态规划 (Dynamic Programming)
核心思想 :
一个大回文串去掉首尾字符后,剩下的部分依然是回文串。
例如:"abba" 是回文,去掉首尾 "bb" 也是回文。
定义状态 :
dp[i][j] 表示子串 s[i...j] 是否为回文串(True 或 False)。
状态转移方程:
- 如果
s[i] == s[j]且dp[i+1][j-1]是回文,那么dp[i][j]也是回文。 - 边界情况:长度为 1 的子串肯定是回文;长度为 2 的子串如果两个字符相等则是回文。
算法步骤:
- 创建一个二维布尔数组
dp。 - 按子串长度从小到大遍历,或者按结束位置遍历。
- 填充
dp表,同时记录最长回文的起始位置和长度。
优点:
- 逻辑非常严谨,容易理解"子问题"的关系。
- 时间复杂度 \(O(N^2)\)。
缺点:
- 空间复杂度高,需要 \(O(N^2)\) 的二维数组。当 \(N=1000\) 时,需要存储 \(1,000,000\) 个布尔值,占用较多内存。
方法三:Manacher 算法 (马拉车算法)
核心思想 :
这是专门解决"最长回文子串"的最优算法。它利用了回文串的对称性,避免了重复计算,将时间复杂度降低到了 \(O(N)\)。
评价:
- 虽然效率最高,但实现非常复杂,代码难以记忆。
- 在面试中,除非面试官特别要求 \(O(N)\) 解法,否则通常不推荐使用此方法,因为"中心扩展法"已经足够好且不易出错。
- 本题解主要聚焦于前两种方法。
3. 代码实现 (中心扩展法)
下面提供的是中心扩展法的 Python 3 代码。这是面试中最标准、性价比最高的解法。
python
class Solution:
def longestPalindrome(self, s: str) -> str:
# 获取字符串长度
n = len(s)
# 边界情况:如果字符串长度小于 2,它本身就是最长回文
if n < 2:
return s
# 记录最长回文子串的起始位置 (start) 和长度 (max_len)
# 初始化为 0 和 1,因为单个字符肯定是回文
start = 0
max_len = 1
# 定义一个辅助函数,用于从中心向两边扩展
# left 和 right 是中心的左右边界索引
def expand_around_center(left: int, right: int) -> int:
# 当 left 和 right 在合法范围内,且字符相等时,继续向两边扩展
while left >= 0 and right < n and s[left] == s[right]:
left -= 1
right += 1
# 循环结束时,s[left] != s[right] 或者越界了
# 所以实际的回文长度是 (right - 1) - (left + 1) + 1 = right - left - 1
return right - left - 1
# 遍历字符串的每一个位置,将其视为"中心"
for i in range(n):
# 情况 1:以 s[i] 为中心,检查奇数长度的回文 (如 "aba")
# 此时左右指针都从 i 开始
len1 = expand_around_center(i, i)
# 情况 2:以 s[i] 和 s[i+1] 之间为中心,检查偶数长度的回文 (如 "abba")
# 此时左指针从 i 开始,右指针从 i+1 开始
len2 = expand_around_center(i, i + 1)
# 取两种情况中的较大值
current_max = max(len1, len2)
# 如果发现的回文比之前记录的更长,则更新 start 和 max_len
if current_max > max_len:
max_len = current_max
# 计算新的起始位置
# 推导公式:起始位置 = 当前中心 i - (新长度 - 1) // 2
# 例如:中心 i=2, 长度 3 ("aba"), start = 2 - 1 = 1
# 例如:中心 i=1, 长度 4 ("abba"), start = 1 - 1 = 0
start = i - (current_max - 1) // 2
# 返回切片,注意切片是左闭右开,所以结束位置是 start + max_len
return s[start : start + max_len]
4. 代码实现 (动态规划法)
为了更全面地理解,这里也提供动态规划的代码。虽然空间消耗大,但逻辑非常经典。
python
class SolutionDP:
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n < 2:
return s
# dp[i][j] 表示 s[i...j] 是否是回文串
# 初始化为 False
dp = [[False] * n for _ in range(n)]
start = 0
max_len = 1
# 遍历右边界 j
for j in range(n):
# 遍历左边界 i
for i in range(j + 1):
# 如果 s[i] 和 s[j] 不相等,肯定不是回文
if s[i] != s[j]:
dp[i][j] = False
else:
# 如果字符相等:
# 1. 如果子串长度 <= 3 (即 j - i < 3),如 "a", "aa", "aba",直接是回文
# 2. 如果长度 > 3,则取决于内部子串 dp[i+1][j-1] 是否为回文
if j - i < 3:
dp[i][j] = True
else:
dp[i][j] = dp[i + 1][j - 1]
# 如果当前子串是回文,且长度大于记录的最大长度,则更新
if dp[i][j] and (j - i + 1) > max_len:
max_len = j - i + 1
start = i
return s[start : start + max_len]
5. 复杂度分析
针对推荐的 中心扩展法:
- 时间复杂度 : \(O(N^2)\)
- 外层循环遍历了 \(N\) 个字符。
- 内层
expand_around_center函数在最坏情况下(如整个字符串都是'a')会扩展 \(O(N)\) 次。 - 总操作次数约为 \(N \times N\)。
- 空间复杂度 : \(O(1)\)
- 我们只使用了几个变量 (
start,max_len,i,left,right) 来存储状态。 - 没有使用额外的数组或矩阵(除了输入字符串本身)。
- 我们只使用了几个变量 (
针对 动态规划法:
- 时间复杂度 : \(O(N^2)\)
- 需要填充一个 \(N \times N\) 的表格。
- 空间复杂度 : \(O(N^2)\)
- 需要存储 \(N \times N\) 的布尔值表格。
6. 总结与建议
- 首选方案 :在面试或实际刷题中,中心扩展法是最佳选择。它在时间和空间上取得了很好的平衡,且代码比 Manacher 算法简单得多,比动态规划节省空间。
- 理解难点 :
- 为什么要检查两次中心(
i, i和i, i+1)?- 答:因为回文串长度可能是奇数(有明确中心字符)或偶数(中心在两个字符之间)。
start索引如何计算?- 答:
start = i - (current_max - 1) // 2。这是通过数学推导得出的,建议通过画图和示例(如"aba"和"abba")来手动验证一下,加深记忆。
- 答:
- 为什么要检查两次中心(
- 边界处理 :
- 代码开头处理了
n < 2的情况,这是为了防止后续逻辑出错,也提高了短字符串的效率。 expand_around_center中的while循环条件left >= 0 and right < n防止了数组越界。
- 代码开头处理了