LeetCode 第 5 题「最长回文子串」是字符串类的经典面试题,暴力解法 O(n³) 会超时,动态规划 O(n²) 费空间,中心扩散法是性价比最高的解法------O(n²) 时间、O(1) 空间,代码短,面试现场几分钟就能写完。
题目
给你一个字符串
s,找到s中最长的回文子串。示例 1 :
s = "babad"→"bab"("aba"也是正确答案)示例 2 :
s = "cbbd"→"bb"
思路分析
暴力法为什么不行
判断一个字符串是不是回文,双指针从两端往中间收,O(n)。要找最长回文子串,如果枚举所有子串(O(n²) 个),每个都判一遍回文,总复杂度 O(n³)。n 最大 1000,O(n³) = 10⁹,铁定超时。
换个方向:从中心往外看
回文的本质特征是对称 。与其枚举子串的起点和终点,不如枚举回文的中心,然后从中心向两边扩展------每轮只比一对字符,相等就继续扩,不等就停。
关键洞察:每个字符最多扩展 n 次,n 个字符就是 O(n²)。而且是 O(1) 额外空间,比 DP 省一个二维数组。
回文中心有两种
这是最容易漏的细节。回文串的长度分两种:
- 奇数长度 :如
"aba",中心是单个字符'b'。扩展时左右指针初始指向同一个位置:expand(s, i, i) - 偶数长度 :如
"abba",中心在两个'b'之间。扩展时左右指针初始指向相邻位置:expand(s, i, i+1)
每个位置 i 都要分别试这两种中心,取长的那个。
完整代码
java
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) return "";
String res = "";
for (int i = 0; i < s.length(); i++) {
// 奇数长度回文:中心是单个字符 s[i]
String odd = expand(s, i, i);
// 偶数长度回文:中心在 s[i] 和 s[i+1] 之间
String even = expand(s, i, i + 1);
// 取更长的那个
if (odd.length() > res.length()) res = odd;
if (even.length() > res.length()) res = even;
}
return res;
}
/**
* 从中心 (l, r) 向两边扩展,返回最长回文子串
* @param s 原始字符串
* @param l 左指针初始位置
* @param r 右指针初始位置
* @return 以 l,r 为中心的最长回文子串
*/
private String expand(String s, int l, int r) {
// 向两边扩展,直到越界或字符不等
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
l--; // 向左走一步
r++; // 向右走一步
}
// 退出循环时 l 和 r 都多走了一步
// 实际回文区间是 [l+1, r-1]
// substring 左闭右开,所以是 (l+1, r)
return s.substring(l + 1, r);
}
}
关键细节:substring 下标为什么是 (l+1, r)
while 循环退出时,一定是两种情况之一:
l < 0或r >= s.length()(越界了)s.charAt(l) != s.charAt(r)(字符不等了)
不管是哪种,退出时 l 比有效左边界多减了 1,r 比有效右边界多加了 1 。换句话说,真正的回文区间是 [l+1, r-1]。
Java 的 substring(begin, end) 是左闭右开 的,所以 end 取 r(即 r-1+1),最终写成 s.substring(l + 1, r)。
这个细节每次推导都要想几秒,后来直接背下来了:退出时 l 和 r 都跨了一步,substring 用 (l+1, r)。
手动模拟
以 s = "babad" 为例,看 i=2(字符 'b',即中间的 b)时发生了什么:
奇数中心:expand(s, 2, 2)
| 轮次 | l | r | sl | sr | 相等? | 当前回文 |
|---|---|---|---|---|---|---|
| 初始 | 2 | 2 | b |
b |
✅ | "b" |
| 第1轮 | 1 | 3 | a |
a |
✅ | "aba" |
| 第2轮 | 0 | 4 | b |
d |
❌ | 退出 |
退出时 l=-1, r=5(等等------实际第2轮 l 从 1 减到 0,r 从 3 加到 4,发现 'b' != 'd',所以退出时 l=0, r=4 。注意这里 l 和 r 记录的是不相等的那一对的位置。)
回文区间:[l+1, r-1] = [1, 3] → s.substring(1, 4) = "aba" ✅
偶数中心:expand(s, 2, 3)
| 轮次 | l | r | sl | sr | 相等? |
|---|---|---|---|---|---|
| 初始 | 2 | 3 | b |
a |
❌ |
第一轮就不等,直接退出。substring(3, 3) = ""(空串),不影响结果。
最终最长:"bab" 或 "aba",长度 3。
复杂度分析
| 项目 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n²) | 外层循环 n 次,内层 expand 最多扩展 n 次 |
| 空间复杂度 | O(1) | 只用了几个变量,没有额外数组 |
对比其他解法:
| 解法 | 时间 | 空间 | 备注 |
|---|---|---|---|
| 暴力枚举 | O(n³) | O(1) | 会超时 |
| 动态规划 | O(n²) | O(n²) | dpij 二维表,空间大 |
| 中心扩散 | O(n²) | O(1) | 本文解法,性价比最高 |
| Manacher | O(n) | O(n) | 最优但复杂,面试不要求 |
常见坑
-
只写奇数忘了偶数 。
"cbbd"的最长回文是"bb",长度偶数,中心在两个字符之间。如果只调expand(s, i, i),这道题直接丢一半的 case。每个 i 位置必须调两次 expand:奇数 + 偶数。 -
substring 下标搞错 。退出循环时 l 和 r 已经跨过了有效边界,直接用
l和r会多截字符。公式:s.substring(l + 1, r)。 -
空串和单字符 。空串返回
"",单字符如"a"返回"a"------中心扩散法天然处理这两种情况,不需要特判。 -
多解时任选其一。题目只要求返回任意一个最长回文,不需要收集所有。
扩展思考
动态规划也能做这道题 ------dp[i][j] 表示 s[i..j] 是不是回文,从短串往长串推。dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1]。时间也是 O(n²),但空间 O(n²),不如中心扩散省空间。
Manacher 算法能做到 O(n),核心是维护"当前最右回文边界"来跳过重复扩展。面试一般不提,除非面试官明确问"还能再优化吗"------这时候聊一下 Manacher 的对称性复用思想就够了,不用现场手写。
总结
中心扩散法的核心就三句话:
- 枚举每个位置作为回文中心(共 2n-1 个中心)
- 奇数中心和偶数中心都要试
- 退出循环时 l 和 r 多走了一步,substring 用
(l+1, r)
LeetCode 5 是回文类题目的敲门砖,搞懂中心扩散,后面 647. 回文子串 和 516. 最长回文子序列 都能顺着这个思路推下去。
这道题你踩过什么坑?是忘写偶数中心还是 substr 下标搞错了?评论区聊聊,回头复习也方便翻。