力扣5. 最长回文子串

在连续攻克了无重复字符的最长子串(LC 3)之后,我们趁热打铁,来看另一道字符串领域的"镇山之宝"------力扣5. 最长回文子串(Longest Palindromic Substring)

这道题同样是中等(Medium)难度,但在字节、腾讯、Google、Amazon 的面试中,它的出现频率极高 。它不仅是回文字符串问题的"母题",更是动态规划中心扩展两大经典思想的绝佳战场。

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

今天,我们将沿着"从低效到高效"的路径,一步步拆解这道题的四种解法:暴力法动态规划中心扩展法 ,以及传说中的"Manacher(马拉车)算法"。读完这篇,你会发现,回文串问题的解法,本身就是一部精彩的算法进化史。

核心难点:如何高效地"找回文"?

回文串的特点是中心对称 ,比如 "aba""abba"。判断一个子串是否为回文,最朴素的想法是从两端向中间比较。但问题在于,如果对所有 子串都这样判断,时间复杂度将是灾难性的 O(n^3)

因此,这道题的核心挑战在于:如何利用回文串的特性,避免重复的比较,从而在 O(n^2) 甚至 O(n) 的时间内找到答案?

第一层:暴力法 ------ 最直观的"地毯式搜索"

这是我们思考的起点。思路很简单:枚举所有子串,检查它是否是回文串,并记录最长的那个。

java 复制代码
class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() < 2) return s;
        int maxLen = 0, start = 0;
        for (int i = 0; i < s.length(); i++) {
            for (int j = i; j < s.length(); j++) {
                if (isPalindrome(s, i, j) && j - i + 1 > maxLen) {
                    maxLen = j - i + 1;
                    start = i;
                }
            }
        }
        return s.substring(start, start + maxLen);
    }

    private boolean isPalindrome(String s, int left, int right) {
        while (left < right) {
            if (s.charAt(left++) != s.charAt(right--)) return false;
        }
        return true;
    }
}
  • 时间复杂度 : O(n^3)。两层循环枚举所有子串是 O(n^2),而每次判断回文又是 O(n)
  • 空间复杂度 : O(1)

点评 :这个解法在LeetCode上会直接超时 ,但它让我们理解了问题的本质。接下来的所有优化,都围绕着如何减少重复的比较展开。

第二层:动态规划 ------ "空间换时间"的经典之作

暴力法慢,是因为它在判断长回文时,重复判断了其内部的子串。如果能记住这些子串的状态,就能避免重复劳动。

核心思想

如果一个子串是回文,并且它首尾各向外扩展一个字符后,这两个新字符也相等,那么扩展后的新子串也一定是回文。

这个特性可以用一个状态转移方程来描述:

dp[i][j] 表示子串 s[i..j] 是否是回文串。

  • s[i] == s[j] 时:dp[i][j] = dp[i+1][j-1]
  • s[i] != s[j] 时:dp[i][j] = false

边界条件 :单个字符 (j - i < 1) 或两个相同字符 (j - i == 1) 都是回文。

java 复制代码
class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        if (n < 2) return s;
        boolean[][] dp = new boolean[n][n];
        int maxLen = 1, start = 0;

        for (int i = n - 1; i >= 0; i--) { // 从下往上遍历
            for (int j = i; j < n; j++) {  // 从左往右遍历
                if (s.charAt(i) == s.charAt(j)) {
                    if (j - i <= 1) { // 边界情况:长度为1或2
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }
                if (dp[i][j] && j - i + 1 > maxLen) {
                    maxLen = j - i + 1;
                    start = i;
                }
            }
        }
        return s.substring(start, start + maxLen);
    }
}
  • 时间复杂度 : O(n^2)。填充了一个 n x n 的二维数组。
  • 空间复杂度 : O(n^2)。用于存储 dp 表。

点评 :这是非常标准的动态规划解法,思路清晰。但 O(n^2) 的空间复杂度在 n 较大时(如 n=1000)依然是一个负担。

第三层:中心扩展法 ------ 抛弃二维表的"降维打击"

动态规划虽然快,但需要额外的二维数组。我们能否用 O(1) 的空间达到同样的效果?

核心思想

回文串是中心对称的。那么,我们为什么不直接从中心向两边扩展呢?

一个长度为 n 的字符串,有 2n-1 个可能的回文中心:

  • n奇数长度回文的中心:单个字符。
  • n-1偶数长度回文的中心:两个相邻字符的中间。

我们遍历所有可能的中心,从每个中心向两边扩展,直到无法构成回文为止,并记录下最长的那个。

java 复制代码
class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() < 2) return s;
        int start = 0, end = 0;
        for (int i = 0; i < s.length(); i++) {
            // 以 s[i] 为中心,奇数长度扩展
            int len1 = expandAroundCenter(s, i, i);
            // 以 s[i] 和 s[i+1] 为中心,偶数长度扩展
            int len2 = expandAroundCenter(s, i, i + 1);
            int len = Math.max(len1, len2);
            if (len > end - start) {
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        return s.substring(start, end + 1);
    }

    private int expandAroundCenter(String s, int left, int right) {
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            left--;
            right++;
        }
        // 返回扩展后的回文长度
        return right - left - 1;
    }
}
  • 时间复杂度 : O(n^2)。最坏情况下,每个中心可能向外扩展 O(n) 次。
  • 空间复杂度 : O(1)。只使用了常数个变量。

点评 :这是面试中最推荐的解法 。它在 O(n^2) 的时间复杂度和 O(1) 的空间复杂度之间取得了完美的平衡,代码简洁,逻辑清晰,是解决此类问题的"万金油"。

第四层:Manacher算法(马拉车) ------ 时间复杂度 O(n) 的"大杀器"

如果你觉得 O(n^2) 还不够快,那么Manacher算法就是终极答案。它利用回文的对称性,将时间复杂度降到了惊人的 O(n)

核心思想

  1. 统一奇偶性 :在原字符串的首尾及每个字符间插入一个特殊字符(如 #),这样所有回文子串都变成了奇数长度。例如,"aba" 变为 "#a#b#a#"
  2. 利用对称性 :维护一个回文半径数组 pArr[i],表示以 i 为中心的最长回文半径。
  3. 加速扩展 :维护当前最靠右的回文右边界 R 和其对称中心 C。当计算 pArr[i] 时,如果 i < R,可以利用其关于 C 的对称点 jpArr[j] 来快速初始化 pArr[i],从而避免从零开始扩展。
python 复制代码
class Solution:
    def longestPalindrome(self, s: str) -> str:
        # 1. 预处理
        T = '#'.join(f'^{s}$') # 加入 '^' 和 '$' 防止越界
        n = len(T)
        P = [0] * n
        C = R = 0
        
        # 2. 计算回文半径数组
        for i in range(1, n-1):
            # 利用对称性快速初始化
            mirror = 2 * C - i
            if i < R:
                P[i] = min(R - i, P[mirror])
            
            # 中心扩展
            while T[i + 1 + P[i]] == T[i - 1 - P[i]]:
                P[i] += 1
            
            # 更新最右边界和中心
            if i + P[i] > R:
                C, R = i, i + P[i]
        
        # 3. 找到最大半径和中心
        max_len, center_index = max((n, i) for i, n in enumerate(P))
        # 4. 从扩展串还原原字符串
        return s[(center_index - max_len) // 2: (center_index + max_len) // 2]
  • 时间复杂度 : O(n)。每个字符最多被访问常数次。
  • 空间复杂度 : O(n)。用于存储扩展后的字符串和半径数组。

点评 :Manacher算法是解决最长回文子串问题的理论最优解。但它理解起来有一定门槛,代码也相对复杂。在面试中,如果能流畅地写出中心扩展法,就已经非常出色了。Manacher算法可以作为"锦上添花"的进阶知识来展示你的深度。

深度总结:四种解法进化图谱

解法 时间复杂度 空间复杂度 核心思想 面试推荐度
暴力法 O(n^3) O(1) 枚举所有子串并验证 ⭐(仅用于理解题意)
动态规划 O(n^2) O(n^2) 记录子状态,避免重复判断 ⭐⭐⭐(思路经典,空间较大)
中心扩展法 O(n^2) O(1) 从中心向两边扩散 ⭐⭐⭐⭐⭐(最推荐,平衡优雅)
Manacher算法 O(n) O(n) 利用对称性加速扩展 ⭐⭐⭐⭐(理论最优,面试加分项)

从这道题中我们学到了什么?

  1. 从暴力到优化的思考路径 :这道题完美展示了如何通过观察问题的特性 (回文的对称性),一步步将算法从 O(n^3) 优化到 O(n)
  2. 动态规划的"空间换时间":当发现子问题被重复计算时,就可以考虑用动态规划来存储中间状态。
  3. 中心扩展的"因地制宜" :在处理回文问题时,O(n^2) 的中心扩展法往往比动态规划更优,因为它省去了二维数组的开销。
  4. Manacher算法的"极致追求":它告诉我们,通过巧妙的预处理和对称性利用,可以将看似无法再优化的算法推向理论极限。

最后的一些心里话

力扣5是一道非常经典的题目,它不仅仅是让你学会做一道题,更是让你体会算法优化的完整过程。从暴力到动态规划,再到中心扩展,最后是Manacher,每一步都代表着一种新的思维方式。

对于面试而言,中心扩展法 是你的"首选武器 ",它足够高效且易于实现。而Manacher算法,更像是一个"秘密武器",在你想要展示极致追求时可以亮出来。

记住今天的口诀:

最长回文子串题,中心扩展是根基。 动态规划也可取,马拉车法最无敌。


如果你觉得这篇题解帮你彻底搞懂了最长回文子串,欢迎点赞、收藏、转发!我们下一站将剑指力扣53「最大子数组和」,不见不散! 🚀

相关推荐
我星期八休息17 小时前
网络编程—应用层HTTP协议
linux·运维·开发语言·前端·网络·网络协议·http
不好听61317 小时前
前端路由完全指南(下篇):懒加载、History 栈与工程化实践
前端·react.js
触底反弹19 小时前
🔥 从零搭建 RAG 知识库:爬虫→分词→向量化→检索,一步都不能错
javascript·人工智能·面试
圣光SG19 小时前
Java Web入门基础知识笔记
java·前端·笔记
cyforkk20 小时前
Vercel 绑定自定义域名极简配置指南
服务器·前端·网络
এ慕ོ冬℘゜21 小时前
JavaScript 数组核心方法实战|新增元素 + 数组转字符串 零基础详解
开发语言·javascript·ecmascript
IT_陈寒21 小时前
React useEffect依赖数组中埋的坑,这次终于让我逮到了
前端·人工智能·后端
FourAu1 天前
2026 前端突围指南:从 ESR 边缘渲染到封装 Web AI SDK,聊聊 AI 时代的职业进化
前端·人工智能