LeetCode 5 最长回文子串 - 中心扩散

LeetCode 第 5 题「最长回文子串」是字符串类的经典面试题,暴力解法 O(n³) 会超时,动态规划 O(n²) 费空间,中心扩散法是性价比最高的解法------O(n²) 时间、O(1) 空间,代码短,面试现场几分钟就能写完。


题目

🟡 5. 最长回文子串

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

示例 1s = "babad""bab""aba" 也是正确答案)

示例 2s = "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 循环退出时,一定是两种情况之一:

  1. l < 0r >= s.length()(越界了)
  2. s.charAt(l) != s.charAt(r)(字符不等了)

不管是哪种,退出时 l 比有效左边界多减了 1,r 比有效右边界多加了 1 。换句话说,真正的回文区间是 [l+1, r-1]

Java 的 substring(begin, end)左闭右开 的,所以 endr(即 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) 最优但复杂,面试不要求

常见坑

  1. 只写奇数忘了偶数"cbbd" 的最长回文是 "bb",长度偶数,中心在两个字符之间。如果只调 expand(s, i, i),这道题直接丢一半的 case。每个 i 位置必须调两次 expand:奇数 + 偶数。

  2. substring 下标搞错 。退出循环时 l 和 r 已经跨过了有效边界,直接用 lr 会多截字符。公式:s.substring(l + 1, r)

  3. 空串和单字符 。空串返回 "",单字符如 "a" 返回 "a"------中心扩散法天然处理这两种情况,不需要特判。

  4. 多解时任选其一。题目只要求返回任意一个最长回文,不需要收集所有。


扩展思考

动态规划也能做这道题 ------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 下标搞错了?评论区聊聊,回头复习也方便翻。

相关推荐
旖旎夜光2 小时前
LeetCode 202:快乐数(双指针问题) —— 题解
数据结构·c++·算法·leetcode·双指针
cpp_25013 小时前
P1113 [USACO02FEB] 杂务
数据结构·c++·算法·动态规划·图论·拓扑排序·洛谷题解
月光船幽幽3 小时前
门控函数SHS阈值与调制机制解析
人工智能·python·算法
测试运维日常笔记3 小时前
tcpdump(Linux服务器)配合 Wireshark(本地分析)使用手顺与常用过滤语法详解
linux·服务器·tcpdump
Doraemomo3 小时前
数据结构-哈希表
数据结构·算法·散列表
君君思密达3 小时前
Leetcode Hot 100 题目详解-哈希表
数据结构·算法·leetcode
lucas_AI3 小时前
Muse Glimmer 30B:Meta 难得给的真开源,强在哪、虚在哪
人工智能·算法
ai产品老杨3 小时前
国产NPU视觉算法完整流程
算法
过期的秋刀鱼!3 小时前
带替换的采样
人工智能·python·算法·决策树·机器学习
过期的秋刀鱼!4 小时前
使用多个决策树
人工智能·算法·决策树·机器学习·数据挖掘