力扣-5. 最长回文子串(Longest Palindromic Substring)

最后编辑于2025-5-23

5. 最长回文子串(Longest Palindromic Substring)

Medium

给定一个字符串 s,返回 s最长的回文子串


示例 1:

输入: s = "babad"
输出: "bab"
解释: "aba" 也是可接受的答案。

示例 2:

输入: s = "cbbd"
输出: "bb"


约束:

  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成

问题分析与常用解法概览

回文串是正读反读都相同的字符串,如 "bob""level""noon" 等。求最长回文子串的常见方法包括:

  1. 中心扩展(Expand Around Center)——时间复杂度 O(n^2),实现简单且常用。需同时处理奇偶长度回文。推荐在面试中先写这个并通过边界测试。
  2. 改进中心扩展的快速跳过重复字符方法——在遇到连续相同字符时先跳过以合并奇偶情形,能减少常数项操作。
  3. 动态规划(DP)——维护 dp[i][j] 表示子串 s[i..j] 是否为回文,时间与空间均为 O(n^2),思路直观但空间开销大。
  4. 马拉车(Manacher)算法——将时间复杂度降到 O(n),算法较巧妙但写起来相对复杂,适合追求最优时间的场景或刷题进阶。

下面给出四种实现(C++),并分别说明复杂度与要点。


方法一:中心扩展(基础版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n < 2) return s;
int start = 0, maxLen = 1;
for (int i = 0; i < n; ++i) {
// 偶数回文(中心在 i 和 i+1 之间)
expandAroundCenter(s, i, i+1, start, maxLen);
// 奇数回文(中心在 i)
expandAroundCenter(s, i, i, start, maxLen);
}
return s.substr(start, maxLen);
}

void expandAroundCenter(const string& s, int left, int right, int& start, int& maxLen) {
while (left >= 0 && right < (int)s.size() && s[left] == s[right]) {
--left; ++right;
}
int len = right - left - 1;
if (len > maxLen) {
maxLen = len;
start = left + 1;
}
}
};
  • 时间复杂度: O(n^2)(最坏情况下每个中心扩展 O(n))
  • 空间复杂度: O(1)

方法二:中心扩展 + 跳过重复字符(优化常数项)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n < 2) return s;
int start = 0, maxLen = 1;
for (int i = 0; i < n;) {
if (n - i <= maxLen / 2) break; // 剩余长度不足以改写 maxLen
int left = i, right = i;
// 跳过右侧的重复字符(合并偶数中心)
while (right + 1 < n && s[right + 1] == s[right]) ++right;
i = right + 1; // 下一个中心从跳过重复字符后开始
// 向两侧扩展
while (left - 1 >= 0 && right + 1 < n && s[left - 1] == s[right + 1]) {
--left; ++right;
}
if (right - left + 1 > maxLen) {
maxLen = right - left + 1;
start = left;
}
}
return s.substr(start, maxLen);
}
};
  • 时间复杂度: 仍为 O(n^2) 最坏,但常数项更小,实际更快。
  • 空间复杂度: O(1)

方法三:动态规划(DP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
string longestPalindrome(string s) {
if (s.empty()) return "";
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n, false));
int left = 0, len = 1;
for (int i = 0; i < n; ++i) {
dp[i][i] = true;
for (int j = 0; j < i; ++j) {
dp[j][i] = (s[i] == s[j]) && (i - j < 2 || dp[j + 1][i - 1]);
if (dp[j][i] && i - j + 1 > len) {
len = i - j + 1;
left = j;
}
}
}
return s.substr(left, len);
}
};
  • 时间复杂度: O(n^2)
  • 空间复杂度: O(n^2)

备注: DP 直观但内存占用大,适合想要用状态定义来证明正确性的场景。


方法四:马拉车(Manacher)算法(O(n))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
string longestPalindrome(string s) {
if (s.empty()) return "";
// 将字符串转换为带分隔符的形式,方便统一处理奇偶回文
string t = "^#"; // ^ 作为起始哨兵
for (char c : s) { t += c; t += '#'; }
t += '$'; // $ 作为结尾哨兵

int n = t.size();
vector<int> p(n, 0);
int center = 0, right = 0; // 当前回文中心及其右边界
int maxLen = 0, centerIndex = 0;
for (int i = 1; i < n - 1; ++i) {
int mirror = 2 * center - i;
if (i < right) p[i] = min(right - i, p[mirror]);
else p[i] = 0;
// 中心扩展
while (t[i + 1 + p[i]] == t[i - 1 - p[i]]) ++p[i];
// 更新中心和右边界
if (i + p[i] > right) {
center = i;
right = i + p[i];
}
if (p[i] > maxLen) {
maxLen = p[i];
centerIndex = i;
}
}
int start = (centerIndex - maxLen) / 2; // 映射回原字符串的起始位置
return s.substr(start, maxLen);
}
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)(用于转换后的字符串与半径数组)

要点: Manacher 在预处理后把奇偶回文统一成奇数情形,使用已知回文信息来跳过重复扩展,显著减少重复比较。


总结与建议

  • 面试中优先写中心扩展解法(方法一或方法二),清晰易懂且能通过大多数测试;若需要更优的时间复杂度或想展示更高算法能力,再写 Manacher(方法四)。
  • 动态规划适合证明和练手,但实现占用内存较大。
  • 建议手写并调试中心扩展与 Manacher,熟悉两者的边界处理技巧(空串、全相同字符、偶数/奇数回文等)。

作者想说的话:
最长回文子串是字符串类题目的经典题型,理解“以中心扩展”与“预处理统一奇偶”的思想,会为你解其他字符串分割/匹配问题打下良好基础。