LeetCode 5 最长回文子串:python3 题解

题目链接:5. 最长回文子串

目录

  • [1. 题目理解](#1. 题目理解)
  • [2. 解题思路与方法讨论](#2. 解题思路与方法讨论)
  • [3. 代码实现 (中心扩展法)](#3. 代码实现 (中心扩展法))
  • [4. 代码实现 (动态规划法)](#4. 代码实现 (动态规划法))
  • [5. 复杂度分析](#5. 复杂度分析)
  • [6. 总结与建议](#6. 总结与建议)

1. 题目理解

题目目标

给定一个字符串 s,你需要找到它里面最长 的一个子串 ,这个子串必须是回文的。

关键概念

  • 子串 (Substring) :字符串中连续的一段字符。例如 "babad" 中,"bab" 是子串,但 "bd" 不是(因为不连续)。
  • 回文 (Palindrome) :正着读和反着读都一样的字符串。例如 "aba", "bb", "a" 都是回文。

示例分析

  • 输入 "babad" -> 输出 "bab""aba"
  • 输入 "cbbd" -> 输出 "bb"

数据范围

字符串长度 1 <= s.length <= 1000。这意味着 \(O(N^2)\) 的算法是可以接受的(\(1000^2 = 1,000,000\) 次运算,在现代计算机上很快),但 \(O(N^3)\) 可能会超时。


2. 解题思路与方法讨论

这道题是经典的字符串算法题,主要有三种解法,难度和效率依次递增。

方法一:中心扩展法 (Expand Around Center)

核心思想

回文串就像照镜子,它是关于"中心"对称的。

  • 如果回文串长度是奇数 (如 "aba"),中心就是中间那个字符('b')。
  • 如果回文串长度是偶数 (如 "abba"),中心就是中间两个字符之间的空隙(两个 'b' 之间)。

算法步骤

  1. 遍历字符串的每一个位置 i
  2. i 视为奇数长度回文串的中心,向左右两边扩展,直到字符不相等。
  3. ii+1 视为偶数长度回文串的中心,向左右两边扩展,直到字符不相等。
  4. 在扩展过程中,记录遇到的最长回文串的起始位置和长度。

优点

  • 空间复杂度低,只需要 \(O(1)\) 的额外空间。
  • 代码逻辑直观,容易编写。
  • 实际运行效率通常比动态规划高。

缺点

  • 时间复杂度为 \(O(N^2)\)。

方法二:动态规划 (Dynamic Programming)

核心思想

一个大回文串去掉首尾字符后,剩下的部分依然是回文串。

例如:"abba" 是回文,去掉首尾 "bb" 也是回文。

定义状态
dp[i][j] 表示子串 s[i...j] 是否为回文串(TrueFalse)。

状态转移方程

  • 如果 s[i] == s[j]dp[i+1][j-1] 是回文,那么 dp[i][j] 也是回文。
  • 边界情况:长度为 1 的子串肯定是回文;长度为 2 的子串如果两个字符相等则是回文。

算法步骤

  1. 创建一个二维布尔数组 dp
  2. 按子串长度从小到大遍历,或者按结束位置遍历。
  3. 填充 dp 表,同时记录最长回文的起始位置和长度。

优点

  • 逻辑非常严谨,容易理解"子问题"的关系。
  • 时间复杂度 \(O(N^2)\)。

缺点

  • 空间复杂度高,需要 \(O(N^2)\) 的二维数组。当 \(N=1000\) 时,需要存储 \(1,000,000\) 个布尔值,占用较多内存。

方法三:Manacher 算法 (马拉车算法)

核心思想

这是专门解决"最长回文子串"的最优算法。它利用了回文串的对称性,避免了重复计算,将时间复杂度降低到了 \(O(N)\)

评价

  • 虽然效率最高,但实现非常复杂,代码难以记忆。
  • 在面试中,除非面试官特别要求 \(O(N)\) 解法,否则通常不推荐使用此方法,因为"中心扩展法"已经足够好且不易出错。
  • 本题解主要聚焦于前两种方法。

3. 代码实现 (中心扩展法)

下面提供的是中心扩展法的 Python 3 代码。这是面试中最标准、性价比最高的解法。

python 复制代码
class Solution:
    def longestPalindrome(self, s: str) -> str:
        # 获取字符串长度
        n = len(s)
        
        # 边界情况:如果字符串长度小于 2,它本身就是最长回文
        if n < 2:
            return s
        
        # 记录最长回文子串的起始位置 (start) 和长度 (max_len)
        # 初始化为 0 和 1,因为单个字符肯定是回文
        start = 0
        max_len = 1
        
        # 定义一个辅助函数,用于从中心向两边扩展
        # left 和 right 是中心的左右边界索引
        def expand_around_center(left: int, right: int) -> int:
            # 当 left 和 right 在合法范围内,且字符相等时,继续向两边扩展
            while left >= 0 and right < n and s[left] == s[right]:
                left -= 1
                right += 1
            # 循环结束时,s[left] != s[right] 或者越界了
            # 所以实际的回文长度是 (right - 1) - (left + 1) + 1 = right - left - 1
            return right - left - 1
        
        # 遍历字符串的每一个位置,将其视为"中心"
        for i in range(n):
            # 情况 1:以 s[i] 为中心,检查奇数长度的回文 (如 "aba")
            # 此时左右指针都从 i 开始
            len1 = expand_around_center(i, i)
            
            # 情况 2:以 s[i] 和 s[i+1] 之间为中心,检查偶数长度的回文 (如 "abba")
            # 此时左指针从 i 开始,右指针从 i+1 开始
            len2 = expand_around_center(i, i + 1)
            
            # 取两种情况中的较大值
            current_max = max(len1, len2)
            
            # 如果发现的回文比之前记录的更长,则更新 start 和 max_len
            if current_max > max_len:
                max_len = current_max
                # 计算新的起始位置
                # 推导公式:起始位置 = 当前中心 i - (新长度 - 1) // 2
                # 例如:中心 i=2, 长度 3 ("aba"), start = 2 - 1 = 1
                # 例如:中心 i=1, 长度 4 ("abba"), start = 1 - 1 = 0
                start = i - (current_max - 1) // 2
        
        # 返回切片,注意切片是左闭右开,所以结束位置是 start + max_len
        return s[start : start + max_len]

4. 代码实现 (动态规划法)

为了更全面地理解,这里也提供动态规划的代码。虽然空间消耗大,但逻辑非常经典。

python 复制代码
class SolutionDP:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        if n < 2:
            return s
        
        # dp[i][j] 表示 s[i...j] 是否是回文串
        # 初始化为 False
        dp = [[False] * n for _ in range(n)]
        
        start = 0
        max_len = 1
        
        # 遍历右边界 j
        for j in range(n):
            # 遍历左边界 i
            for i in range(j + 1):
                # 如果 s[i] 和 s[j] 不相等,肯定不是回文
                if s[i] != s[j]:
                    dp[i][j] = False
                else:
                    # 如果字符相等:
                    # 1. 如果子串长度 <= 3 (即 j - i < 3),如 "a", "aa", "aba",直接是回文
                    # 2. 如果长度 > 3,则取决于内部子串 dp[i+1][j-1] 是否为回文
                    if j - i < 3:
                        dp[i][j] = True
                    else:
                        dp[i][j] = dp[i + 1][j - 1]
                
                # 如果当前子串是回文,且长度大于记录的最大长度,则更新
                if dp[i][j] and (j - i + 1) > max_len:
                    max_len = j - i + 1
                    start = i
                    
        return s[start : start + max_len]

5. 复杂度分析

针对推荐的 中心扩展法

  • 时间复杂度 : \(O(N^2)\)
    • 外层循环遍历了 \(N\) 个字符。
    • 内层 expand_around_center 函数在最坏情况下(如整个字符串都是 'a')会扩展 \(O(N)\) 次。
    • 总操作次数约为 \(N \times N\)。
  • 空间复杂度 : \(O(1)\)
    • 我们只使用了几个变量 (start, max_len, i, left, right) 来存储状态。
    • 没有使用额外的数组或矩阵(除了输入字符串本身)。

针对 动态规划法

  • 时间复杂度 : \(O(N^2)\)
    • 需要填充一个 \(N \times N\) 的表格。
  • 空间复杂度 : \(O(N^2)\)
    • 需要存储 \(N \times N\) 的布尔值表格。

6. 总结与建议

  1. 首选方案 :在面试或实际刷题中,中心扩展法是最佳选择。它在时间和空间上取得了很好的平衡,且代码比 Manacher 算法简单得多,比动态规划节省空间。
  2. 理解难点
    • 为什么要检查两次中心(i, ii, i+1)?
      • 答:因为回文串长度可能是奇数(有明确中心字符)或偶数(中心在两个字符之间)。
    • start 索引如何计算?
      • 答:start = i - (current_max - 1) // 2。这是通过数学推导得出的,建议通过画图和示例(如 "aba""abba")来手动验证一下,加深记忆。
  3. 边界处理
    • 代码开头处理了 n < 2 的情况,这是为了防止后续逻辑出错,也提高了短字符串的效率。
    • expand_around_center 中的 while 循环条件 left >= 0 and right < n 防止了数组越界。