题目描述
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
text
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
text
输入:s = "cbbd"
输出:"bb"
提示:
-
1 <= s.length <= 1000 -
s仅由数字和英文字母组成
思路分析
本题是字符串处理的经典问题,主要有以下三种解法:
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力枚举 | O(n³) | O(1) | 仅用于理解 |
| 中心扩展法 | O(n²) | O(1) | 简单直观,面试首选 |
| 动态规划 | O(n²) | O(n²) | 需要打印所有回文子串时 |
| Manacher 算法 | O(n) | O(n) | 对性能要求极高时 |
下文将重点讲解 中心扩展法 和 动态规划法。
方法一:中心扩展法(推荐)
核心思想
回文串一定是对称的。我们可以枚举每一个可能的"回文中心",然后向左右两边扩展,直到两边的字符不相等为止。由于回文串长度可能为奇数或偶数,中心可能是一个字符(奇数长度)或两个字符之间(偶数长度)。
对于长度为 n 的字符串,共有 2n - 1 个这样的中心。
算法步骤
-
遍历每个中心位置(
0到2n-2)。 -
对于每个中心,确定初始左右指针:
-
若中心为字符:
left = center / 2,right = left。 -
若中心为间隙:
left = center / 2,right = left + 1。
-
-
向两边扩展,当
s[left] == s[right]时继续扩大范围。 -
每次扩展后,若当前回文长度大于记录的最大值,则更新起始位置和最大长度。
-
最终返回最长回文子串。
代码实现
Java
java
class Solution {
public String longestPalindrome(String s) {
// 1. 边界处理:空串或 null 直接返回空字符串
if (s == null || s.length() < 1) {
return "";
}
// 2. 记录当前找到的最长回文子串的起始和结束索引
int start = 0, end = 0; // 初始假设第一个字符就是最长回文(长度为1)
// 3. 遍历每一个可能的中心点(包括字符本身和字符间的空隙)
for (int i = 0; i < s.length(); i++) {
// 3.1 以 i 为字符中心(处理奇数长度回文,如 "aba")
int len1 = expandCenter(s, i, i);
// 3.2 以 i 和 i+1 之间的空隙为中心(处理偶数长度回文,如 "abba")
int len2 = expandCenter(s, i, i + 1);
// 3.3 取两种中心方式中较长的长度
int len = Math.max(len1, len2);
// 3.4 如果找到更长的回文,更新起止索引
if (len > end - start) { // 注意:end - start 代表当前最大长度-1(因为索引从0开始)
// 反推起始索引:i 是中心,左边有 (len-1)/2 个字符
start = i - (len - 1) / 2;
// 反推结束索引:i + 右边字符数
end = i + len / 2;
}
}
// 4. 根据记录的起止索引截取子串(注意 substring 的结束索引是不包含的,所以要 +1)
return s.substring(start, end + 1);
}
/**
* 从中心向两边扩展,返回能形成的最长回文子串的【长度】
* @param s 原字符串
* @param left 向左扩展的起始指针
* @param right 向右扩展的起始指针
* @return 回文子串的长度
*/
public int expandCenter(String s, int left, int right) {
// 只要左右指针不越界,并且指向的字符相等,就继续向外扩
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--; // 左指针左移
right++; // 右指针右移
}
// 循环退出时,left 和 right 都多走了一步(指向不相等或越界的位置)
// 因此实际回文串的长度为:(right - 1) - (left + 1) + 1 = right - left - 1
return right - left - 1;
}
}
复杂度分析
-
时间复杂度:O(n²),每次扩展最多 O(n) 次比较。
-
空间复杂度:O(1),只使用了常数变量。
方法二:动态规划
核心思想
定义 dp[i][j] 表示子串 s[i..j] 是否为回文串。则状态转移方程为:
text
dp[i][j] = (s[i] == s[j]) && (j - i < 3 || dp[i+1][j-1])
即:若首尾字符相同,且去掉首尾后子串也为回文(或长度小于等于 3 直接成立),则当前子串为回文。
算法步骤
-
初始化一个
n × n的布尔型dp数组,默认全为false。 -
单个字符(
i == j)必然是回文,置true。 -
按子串长度从小到大进行递推(
len = 2到n)。 -
对于每个长度,遍历所有可能的起始位置
i,计算结束位置j = i + len - 1。 -
若
dp[i][j]为true且len > maxLen,则更新最长回文子串的起始位置和长度。 -
返回最长回文子串。
代码实现
Java
java
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
if (n < 2) return s;
boolean[][] dp = new boolean[n][n];
int start = 0, maxLen = 1;
// 单个字符均为回文
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 按长度递推
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
if (s.charAt(i) != s.charAt(j)) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true; // 长度为 2 或 3 且两端相等,直接为回文
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
if (dp[i][j] && len > maxLen) {
start = i;
maxLen = len;
}
}
}
return s.substring(start, start + maxLen);
}
}
复杂度分析
-
时间复杂度:O(n²),双重循环。
-
空间复杂度 :O(n²),
dp数组占用。
方法三:Manacher 算法(线性时间)
Manacher 算法可在 O(n) 时间内求出最长回文子串,但实现较为复杂。核心思路是利用回文的对称性,通过已计算的臂长信息避免重复扩展。有兴趣的读者可参考专门讲解 Manacher 算法的文章。
总结对比
| 方法 | 优点 | 缺点 |
|---|---|---|
| 中心扩展法 | 代码简洁,空间 O(1),易于手写 | 时间复杂度 O(n²),最坏情况较慢 |
| 动态规划 | 易于理解,可得到所有子串的回文状态 | 空间消耗 O(n²) |
| Manacher | 时间复杂度 O(n),最优 | 实现复杂,不易在面试中写出 |
对于绝大多数场景(包括面试),中心扩展法 已足够应对,且是推荐的首选解法。
扩展思考
-
如果要求返回 所有 最长回文子串(可能有多个),该如何修改代码?
-
如果字符串长度达到 10⁵ 级别,必须使用 Manacher 算法。
-
最长回文子序列 与 最长回文子串 有何区别?(子序列不要求连续)