【力扣100题】53.最长回文子串

题目描述

给你一个字符串 s,找到 s 中最长的回文子串。

示例

复制代码
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:
输入:s = "cbbd"
输出:"bb"

约束:1 <= s.length <= 1000

解题思路总览

方法 核心思想 时间复杂度 空间复杂度 备注
暴力枚举 枚举所有子串,判断是否回文 O(n^3) O(1) 会超时
动态规划 dpij = si==sj && dpi+1j-1 O(n^2) O(n^2) 空间较大
中心扩展 从中心向两边扩展 O(n^2) O(1) 最常用
Manacher 马拉车算法,O(n) O(n) O(n) 最优但复杂

一、中心扩展法(推荐)

核心思想

回文串是中心对称的。选择一个中心,向两边同时扩展,遇到不匹配的字符就停止。这样可以找到以该中心为中心的最长回文串。

两种情况

回文串有两种形态:

  • 奇数长度:中心是一个字符,如 "aba",中心是 'b'
  • 偶数长度:中心是两个字符,如 "abba",中心是 'bb'

代码实现

cpp 复制代码
class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        int ans_l = 0, ans_r = 0;  // 记录最长回文子串的左右边界

        // 情况1:奇数长度回文,中心是单字符
        for (int i = 0; i < n; i++) {
            int l = i, r = i;  // 以 i 为中心
            // 向两边扩展,直到不匹配
            while (l >= 0 && r < n && s[l] == s[r]) {
                l--;
                r++;
            }
            // 此时 [l+1, r-1] 是以 i 为中心的回文串
            // 长度 = r - l - 1
            if (r - l - 1 > ans_r - ans_l) {
                ans_l = l + 1;
                ans_r = r;
            }
        }

        // 情况2:偶数长度回文,中心是两个相邻字符
        for (int i = 0; i < n - 1; i++) {
            int l = i, r = i + 1;  // 以 i 和 i+1 为中心
            while (l >= 0 && r < n && s[l] == s[r]) {
                l--;
                r++;
            }
            if (r - l - 1 > ans_r - ans_l) {
                ans_l = l + 1;
                ans_r = r;
            }
        }

        return s.substr(ans_l, ans_r - ans_l);
    }
};

二、算法流程图

以 s = "babad" 为例

复制代码
原始字符串:b a b a d
索引:      0 1 2 3 4

第一步:枚举所有可能的回文中心

奇数长度中心(每个字符):
  i=0: "b"   -> 向两边扩:l=0,r=0 -> s[0]==s[0],继续
                        l=-1,r=1 -> 超界,停止
               回文:"b",长度 1

  i=1: "a"   -> 向两边扩:s[1]==s[1] -> l=0,r=2
                        s[0]==s[2] -> l=-1,r=3 -> 超界,停止
               回文:"bab",长度 3

  i=2: "a"   -> 同上,回文:"aba",长度 3

  i=3: "a"   -> 向两边扩:s[3]==s[3] -> l=2,r=4
                        s[2]!=s[4],停止
               回文:"a",长度 1

  i=4: "d"   -> 向两边扩:s[4]==s[4],停止
               回文:"d",长度 1

偶数长度中心(相邻字符对):
  i=0: "ba"  -> s[0]!=s[1],停止
               回文:无

  i=1: "ab"  -> s[1]!=s[2],停止
               回文:无

  i=2: "ba"  -> s[2]!=s[3],停止
               回文:无

  i=3: "ad"  -> s[3]!=s[4],停止
               回文:无

最长回文:"bab" 或 "aba",长度 3

以 s = "cbbd" 为例

复制代码
原始字符串:c b b d
索引:      0 1 2 3

奇数长度中心:
  i=0: "c"   -> 长度 1
  i=1: "b"   -> 向两边扩:s[1]==s[1] -> l=0,r=2
                        s[0]!=s[2],停止
               回文:"b",长度 1

  i=2: "b"   -> 同上,回文:"b",长度 1
  i=3: "d"   -> 长度 1

偶数长度中心:
  i=0: "cb"  -> s[0]!=s[1],停止
  i=1: "bb"  -> 向两边扩:s[1]==s[2] -> l=0,r=3
                        s[0]!=s[3],停止
               回文:"bb",长度 2
  i=2: "bd"  -> s[2]!=s[3],停止

最长回文:"bb",长度 2

三、逐行解析(对照原题代码)

cpp 复制代码
string longestPalindrome(string s) {
    int n = s.size();

    // ans_l 和 ans_r 记录当前找到的最长回文子串的左右边界(左闭右开)
    int ans_l = 0, ans_r = 0;

    // ---------- 情况1:奇数长度回文 ----------
    // 中心是单字符,枚举每个位置作为中心
    for (int i = 0; i < n; i++) {
        int l = i, r = i;  // 初始中心是字符 s[i]
        // while 循环:只要 l 和 r 都在范围内,且 s[l] == s[r],就继续扩展
        while (l >= 0 && r < n && s[l] == s[r]) {
            l--;
            r++;
        }
        // 退出循环时:[l+1, r-1] 是以 i 为中心的最长回文串
        // 长度 = r - l - 1
        // 如果比当前答案更长,就更新
        if (r - l - 1 > ans_r - ans_l) {
            ans_l = l + 1;
            ans_r = r;
        }
    }

    // ---------- 情况2:偶数长度回文 ----------
    // 中心是两个相邻字符,枚举每对相邻字符
    for (int i = 0; i < n - 1; i++) {
        int l = i, r = i + 1;  // 初始中心是 s[i] 和 s[i+1]
        while (l >= 0 && r < n && s[l] == s[r]) {
            l--;
            r++;
        }
        if (r - l - 1 > ans_r - ans_l) {
            ans_l = l + 1;
            ans_r = r;
        }
    }

    // 返回从 ans_l 开始,长度为 ans_r - ans_l 的子串
    return s.substr(ans_l, ans_r - ans_l);
}

关键点解释

语句 含义
int ans_l = 0, ans_r = 0; 记录最长回文子串的边界,ans_l 是起始索引,ans_r 是结束索引(不包含)
while (l >= 0 && r < n && s[l] == s[r]) 扩展条件:左边界没越界,右边界没越界,左右字符相等
l--; r++; 向两边扩展一位
ans_l = l + 1; ans_r = r; 更新答案,l+1 是因为退出循环时 l 多减了 1
r - l - 1 当前回文串的长度
s.substr(ans_l, ans_r - ans_l) C++ 的 substring,参数是起始位置和长度

四、图解扩展过程

复制代码
以 s = "babad",i = 1(中心是 'a')为例:

初始状态:
     l=i=1, r=i=1
     b a b a d
       ^
       中心

第一次扩展(l=0, r=2):
     s[0] == s[2]?  'b' == 'b'?  是!
     b a b a d
     ^     ^
     l     r

第二次扩展(l=-1, r=3):
     l < 0,越界,停止

最终回文:[l+1, r-1] = [0, 2] = "bab"
长度 = r - l - 1 = 3 - (-1) - 1 = 3

以 s = "cbbd",i = 1(中心是 "bb")为例:

初始状态:
     l=i=1, r=i+1=2
     c b b d
       ^^
       中心

第一次扩展(l=0, r=3):
     s[0] == s[3]?  'c' == 'd'?  否,停止

最终回文:[l+1, r-1] = [1, 2] = "bb"
长度 = r - l - 1 = 4 - 0 - 1 = 3? 错!
实际是 [1,3) = s[1] 和 s[2],长度 = 2

注意:退出 while 时 l=-1, r=4
      回文边界是 [l+1, r-1] = [0, 3],长度 = 3? 不对
      实际 l=0 时已经判断 s[0]!=s[3],所以回文不包括 0 和 3
      正确:[1, 3) 不包括 r

五、复杂度分析

维度 分析
时间复杂度 最坏情况下,每个中心都要扩展 O(n) 次,共 O(n) 个中心,总 O(n^2)
空间复杂度 只用了几个整数变量,O(1)

最坏情况

字符串是全相同字符,如 "aaaaa...":

  • 奇数中心:每个扩展 O(n) 次
  • 偶数中心:每个扩展 O(n) 次
  • 总计 O(n^2)

六、动态规划解法(对比)

思路

定义 dp[i][j] = si...j 是否是回文串。

状态转移

  • dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1]
  • 即首尾相等,且中间也是回文
cpp 复制代码
class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        if (n <= 1) return s;

        vector<vector<int>> dp(n, vector<int>(n, 0));
        int start = 0, maxLen = 1;

        // 所有单字符都是回文
        for (int i = 0; i < n; i++) dp[i][i] = 1;

        // 枚举长度
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i + len <= n; i++) {
                int j = i + len - 1;
                if (s[i] == s[j]) {
                    if (len == 2) dp[i][j] = 1;  // "aa" 这种
                    else dp[i][j] = dp[i + 1][j - 1];
                }
                if (dp[i][j] && len > maxLen) {
                    start = i;
                    maxLen = len;
                }
            }
        }

        return s.substr(start, maxLen);
    }
};

两种方法对比

维度 中心扩展 动态规划
时间复杂度 O(n^2) O(n^2)
空间复杂度 O(1) O(n^2)
编码难度 简单 中等
思维难度 易理解 需理解 DP 状态定义

七、Manacher 算法(了解即可)

核心思想

O(n) 时间复杂度的算法,利用回文串的对称性避免重复计算。

cpp 复制代码
// 伪代码,不完整实现
string manacher(string s) {
    // 1. 插入分隔符,使奇偶统一
    string t = "#";
    for (char c : s) {
        t += c;
        t += "#";
    }

    // 2. 计算每个位置的回文半径
    vector<int> p(t.size(), 0);
    int center = 0, right = 0;

    for (int i = 0; i < t.size(); i++) {
        int mirror = 2 * center - i;
        if (i < right) p[i] = min(p[mirror], right - i);

        // 中心扩展
        while (i - p[i] >= 0 && i + p[i] < t.size() && t[i - p[i]] == t[i + p[i]]) {
            p[i]++;
        }

        // 更新 center 和 right
        if (i + p[i] > right) {
            center = i;
            right = i + p[i];
        }
    }

    // 3. 找出最长回文
    // ...
}

面试中如果能提到 Manacher,可以加分,但中心扩展已经足够。


面试追问 FAQ

问题 回答要点
Q: 为什么中心扩展能找所有回文子串? 任何回文串都有中心(单字符或双字符),枚举所有可能的中心,扩展找最长
Q: 如何处理奇数和偶数两种情况? 分别处理:奇数中心是单字符,偶数中心是相邻字符对
Q: 时间复杂度为什么是 O(n^2)? 有 O(n) 个中心,每个中心最多扩展 O(n) 次
Q: 能用滑动窗口吗? 不行,因为回文长度不确定,无法用滑动窗口优化
Q: 如果要返回所有最长回文怎么办? 在遍历过程中记录所有等长的回文,而不是只更新一个
Q: Manacher 算法了解吗? 可以简单说:O(n) 算法,利用回文对称性,用半径数组避免重复计算

相关题目

题目编号 题目名称 难度 核心差异
5 最长回文子串 中等 基础题,返回子串
516 最长回文子序列 中等 返回长度或子序列,不要求连续
647 回文子串 中等 计数所有回文子串
409 最长回文串 简单 可以重新排列字符
1312 让字符串成为回文串的最少插入 困难 插入最少字符使字符串回文

总结

要点 内容
核心思想 中心扩展:枚举每个可能的中心(单字符或双字符),向两边扩展找最长
两种情况 奇数长度(单中心)和偶数长度(双中心)
时间复杂度 O(n^2)
空间复杂度 O(1)
关键点 注意边界条件,while 循环结束后 l 多减了 1
易错点 偶数中心时初始化是 l=i, r=i+1,不是 l=i-1, r=i+1

中心扩展法是最直观、最实用的解法,面试中推荐使用。如果面试官问更优解,可以补充 Manacher 算法的思想。


相关推荐
想你依然心痛15 分钟前
量子计算对嵌入式密码学的挑战与后量子算法准备
算法·密码学·量子计算
Jerry4 小时前
LeetCode 28. 找出字符串中第一个匹配项的下标
算法
Jerry6 小时前
LeetCode 459. 重复的子字符串
算法
海石9 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石9 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
Jerry11 小时前
LeetCode 151. 反转字符串中的单词
算法
咖啡星人k13 小时前
从Vibe Coding到Prompt Engineering:AI编程的新技能树
面试·职场和发展·ai编程
a11177613 小时前
LM 算法迭代过程动画演示(SLAM)
算法
头茬韭菜13 小时前
Context 的生死抉择:四层压缩、截断算法与 Session Memory
算法·ai
Jerry13 小时前
LeetCode 541. 反转字符串 II
算法