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

题目描述

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

示例

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

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

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

解题思路总览

方法 核心思想 时间复杂度 空间复杂度 备注
暴力枚举 枚举所有子串,判断是否回文 O(n^3) O(1) 会超时
动态规划 dp[i][j] = s[i]==s[j] && dp[i+1][j-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] = s[i...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 算法的思想。


相关推荐
jieyucx1 小时前
Go 语言 sort 包详解:从基础排序到自定义排序(含底层原理+零基础看懂)
算法·golang·排序算法·sort
仙俊红1 小时前
Integer\int对比,equals()\hashcode面试
java·面试·职场和发展
叁散2 小时前
ESP32 LCD1602显示实验报告
算法
过期动态2 小时前
【LeetCode 热题 100】盛最多水的容器
java·数据结构·spring boot·算法·leetcode·spring cloud·职场和发展
凌波粒2 小时前
LeetCode--700.二叉搜索树中的搜索(二叉树)
算法·leetcode·职场和发展
君为先-bey3 小时前
LeMiCa——基于扩散模型的高效视频生成的词典序最小化路径缓存
python·算法·机器学习·扩散模型
洛水水3 小时前
【力扣100题】58.轮转数组
算法·leetcode
资深流水灯工程师3 小时前
LMS 最小均方算法在 DSP 上的 C 语言实现
算法
风筝在晴天搁浅3 小时前
阿里 LeetCode 876.链表的中间节点
算法·leetcode·链表