在连续攻克了无重复字符的最长子串(LC 3)之后,我们趁热打铁,来看另一道字符串领域的"镇山之宝"------力扣5. 最长回文子串(Longest Palindromic Substring)。
这道题同样是中等(Medium)难度,但在字节、腾讯、Google、Amazon 的面试中,它的出现频率极高 。它不仅是回文字符串问题的"母题",更是动态规划 和中心扩展两大经典思想的绝佳战场。
给定一个字符串
s,找到s中最长的回文子串。
今天,我们将沿着"从低效到高效"的路径,一步步拆解这道题的四种解法:暴力法 、动态规划 、中心扩展法 ,以及传说中的"Manacher(马拉车)算法"。读完这篇,你会发现,回文串问题的解法,本身就是一部精彩的算法进化史。
核心难点:如何高效地"找回文"?
回文串的特点是中心对称 ,比如 "aba" 或 "abba"。判断一个子串是否为回文,最朴素的想法是从两端向中间比较。但问题在于,如果对所有 子串都这样判断,时间复杂度将是灾难性的 O(n^3)。
因此,这道题的核心挑战在于:如何利用回文串的特性,避免重复的比较,从而在 O(n^2) 甚至 O(n) 的时间内找到答案?
第一层:暴力法 ------ 最直观的"地毯式搜索"
这是我们思考的起点。思路很简单:枚举所有子串,检查它是否是回文串,并记录最长的那个。
java
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 2) return s;
int maxLen = 0, start = 0;
for (int i = 0; i < s.length(); i++) {
for (int j = i; j < s.length(); j++) {
if (isPalindrome(s, i, j) && j - i + 1 > maxLen) {
maxLen = j - i + 1;
start = i;
}
}
}
return s.substring(start, start + maxLen);
}
private boolean isPalindrome(String s, int left, int right) {
while (left < right) {
if (s.charAt(left++) != s.charAt(right--)) return false;
}
return true;
}
}
- 时间复杂度 :
O(n^3)。两层循环枚举所有子串是O(n^2),而每次判断回文又是O(n)。 - 空间复杂度 :
O(1)。
点评 :这个解法在LeetCode上会直接超时 ,但它让我们理解了问题的本质。接下来的所有优化,都围绕着如何减少重复的比较展开。
第二层:动态规划 ------ "空间换时间"的经典之作
暴力法慢,是因为它在判断长回文时,重复判断了其内部的子串。如果能记住这些子串的状态,就能避免重复劳动。
核心思想
如果一个子串是回文,并且它首尾各向外扩展一个字符后,这两个新字符也相等,那么扩展后的新子串也一定是回文。
这个特性可以用一个状态转移方程来描述:
令
dp[i][j]表示子串s[i..j]是否是回文串。
- 当
s[i] == s[j]时:dp[i][j] = dp[i+1][j-1]。- 当
s[i] != s[j]时:dp[i][j] = false。
边界条件 :单个字符 (j - i < 1) 或两个相同字符 (j - i == 1) 都是回文。
java
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
if (n < 2) return s;
boolean[][] dp = new boolean[n][n];
int maxLen = 1, start = 0;
for (int i = n - 1; i >= 0; i--) { // 从下往上遍历
for (int j = i; j < n; j++) { // 从左往右遍历
if (s.charAt(i) == s.charAt(j)) {
if (j - i <= 1) { // 边界情况:长度为1或2
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
start = i;
}
}
}
return s.substring(start, start + maxLen);
}
}
- 时间复杂度 :
O(n^2)。填充了一个n x n的二维数组。 - 空间复杂度 :
O(n^2)。用于存储dp表。
点评 :这是非常标准的动态规划解法,思路清晰。但 O(n^2) 的空间复杂度在 n 较大时(如 n=1000)依然是一个负担。
第三层:中心扩展法 ------ 抛弃二维表的"降维打击"
动态规划虽然快,但需要额外的二维数组。我们能否用 O(1) 的空间达到同样的效果?
核心思想
回文串是中心对称的。那么,我们为什么不直接从中心向两边扩展呢?
一个长度为 n 的字符串,有 2n-1 个可能的回文中心:
n个奇数长度回文的中心:单个字符。n-1个偶数长度回文的中心:两个相邻字符的中间。
我们遍历所有可能的中心,从每个中心向两边扩展,直到无法构成回文为止,并记录下最长的那个。
java
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 2) return s;
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
// 以 s[i] 为中心,奇数长度扩展
int len1 = expandAroundCenter(s, i, i);
// 以 s[i] 和 s[i+1] 为中心,偶数长度扩展
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
private int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
// 返回扩展后的回文长度
return right - left - 1;
}
}
- 时间复杂度 :
O(n^2)。最坏情况下,每个中心可能向外扩展O(n)次。 - 空间复杂度 :
O(1)。只使用了常数个变量。
点评 :这是面试中最推荐的解法 。它在 O(n^2) 的时间复杂度和 O(1) 的空间复杂度之间取得了完美的平衡,代码简洁,逻辑清晰,是解决此类问题的"万金油"。
第四层:Manacher算法(马拉车) ------ 时间复杂度 O(n) 的"大杀器"
如果你觉得 O(n^2) 还不够快,那么Manacher算法就是终极答案。它利用回文的对称性,将时间复杂度降到了惊人的 O(n)。
核心思想
- 统一奇偶性 :在原字符串的首尾及每个字符间插入一个特殊字符(如
#),这样所有回文子串都变成了奇数长度。例如,"aba"变为"#a#b#a#"。 - 利用对称性 :维护一个回文半径数组
pArr[i],表示以i为中心的最长回文半径。 - 加速扩展 :维护当前最靠右的回文右边界
R和其对称中心C。当计算pArr[i]时,如果i < R,可以利用其关于C的对称点j的pArr[j]来快速初始化pArr[i],从而避免从零开始扩展。
python
class Solution:
def longestPalindrome(self, s: str) -> str:
# 1. 预处理
T = '#'.join(f'^{s}$') # 加入 '^' 和 '$' 防止越界
n = len(T)
P = [0] * n
C = R = 0
# 2. 计算回文半径数组
for i in range(1, n-1):
# 利用对称性快速初始化
mirror = 2 * C - i
if i < R:
P[i] = min(R - i, P[mirror])
# 中心扩展
while T[i + 1 + P[i]] == T[i - 1 - P[i]]:
P[i] += 1
# 更新最右边界和中心
if i + P[i] > R:
C, R = i, i + P[i]
# 3. 找到最大半径和中心
max_len, center_index = max((n, i) for i, n in enumerate(P))
# 4. 从扩展串还原原字符串
return s[(center_index - max_len) // 2: (center_index + max_len) // 2]
- 时间复杂度 :
O(n)。每个字符最多被访问常数次。 - 空间复杂度 :
O(n)。用于存储扩展后的字符串和半径数组。
点评 :Manacher算法是解决最长回文子串问题的理论最优解。但它理解起来有一定门槛,代码也相对复杂。在面试中,如果能流畅地写出中心扩展法,就已经非常出色了。Manacher算法可以作为"锦上添花"的进阶知识来展示你的深度。
深度总结:四种解法进化图谱
| 解法 | 时间复杂度 | 空间复杂度 | 核心思想 | 面试推荐度 |
|---|---|---|---|---|
| 暴力法 | O(n^3) | O(1) | 枚举所有子串并验证 | ⭐(仅用于理解题意) |
| 动态规划 | O(n^2) | O(n^2) | 记录子状态,避免重复判断 | ⭐⭐⭐(思路经典,空间较大) |
| 中心扩展法 | O(n^2) | O(1) | 从中心向两边扩散 | ⭐⭐⭐⭐⭐(最推荐,平衡优雅) |
| Manacher算法 | O(n) | O(n) | 利用对称性加速扩展 | ⭐⭐⭐⭐(理论最优,面试加分项) |
从这道题中我们学到了什么?
- 从暴力到优化的思考路径 :这道题完美展示了如何通过观察问题的特性 (回文的对称性),一步步将算法从
O(n^3)优化到O(n)。 - 动态规划的"空间换时间":当发现子问题被重复计算时,就可以考虑用动态规划来存储中间状态。
- 中心扩展的"因地制宜" :在处理回文问题时,
O(n^2)的中心扩展法往往比动态规划更优,因为它省去了二维数组的开销。 - Manacher算法的"极致追求":它告诉我们,通过巧妙的预处理和对称性利用,可以将看似无法再优化的算法推向理论极限。
最后的一些心里话
力扣5是一道非常经典的题目,它不仅仅是让你学会做一道题,更是让你体会算法优化的完整过程。从暴力到动态规划,再到中心扩展,最后是Manacher,每一步都代表着一种新的思维方式。
对于面试而言,中心扩展法 是你的"首选武器 ",它足够高效且易于实现。而Manacher算法,更像是一个"秘密武器",在你想要展示极致追求时可以亮出来。
记住今天的口诀:
最长回文子串题,中心扩展是根基。 动态规划也可取,马拉车法最无敌。
如果你觉得这篇题解帮你彻底搞懂了最长回文子串,欢迎点赞、收藏、转发!我们下一站将剑指力扣53「最大子数组和」,不见不散! 🚀