Skip to content

最长回文子串

这道题要求找出字符串中最长的回文子串。注意是子串(连续),不是子序列,返回的是子串本身,不是长度。

回文串就是正着读和反着读一样的字符串,比如 "aba""abba"。单个字符也算回文。

最直观的想法

第一次看到这题,我想到的是:枚举所有可能的子串,判断每个是不是回文,取最长的。

两层循环,外层遍历起点,内层遍历终点,对每个子串用双指针判断是否回文。这是最"暴力"的方法:

javascript
var longestPalindrome = function(s) {
    let maxLen = 0
    let result = ''
    
    for (let i = 0; i < s.length; i++) {
        for (let j = i; j < s.length; j++) {
            if (isPalindrome(s, i, j)) {
                const len = j - i + 1
                if (len > maxLen) {
                    maxLen = len
                    result = s.substring(i, j + 1)
                }
            }
        }
    }
    
    return result
}

function isPalindrome(s, left, right) {
    while (left < right) {
        if (s[left] !== s[right]) return false
        left++
        right--
    }
    return true
}

时间复杂度 O(n³),对于长字符串会超时。

问题在哪?

这个方法做了很多重复计算。比如我们判断 s[0...4] = "abcba" 是否是回文,然后判断 s[1...3] = "bcb" 时,又重新比较了一遍。

而且,回文串有个重要特征:中心对称。如果我们知道 "aba" 是回文,那么 "a" 也一定是回文。如果我们知道 "abba" 是回文,那么 "bb" 也一定是回文。

中心扩展

既然回文是中心对称的,那我们可以从每个可能的"中心"开始,向两边扩展,找到最长的回文。

中心可能是:

  • 单个字符(奇数长度回文,比如 "aba" 的中心是 'b'
  • 两个字符之间(偶数长度回文,比如 "abba" 的中心在 'b''b' 之间)

对每个中心,我们向两边扩展,直到不满足回文条件:

javascript
var longestPalindrome = function(s) {
    if (s.length < 2) return s
    
    let start = 0
    let maxLen = 1
    
    for (let i = 0; i < s.length; i++) {
        // 奇数长度:中心是一个字符
        const len1 = expandAroundCenter(s, i, i)
        // 偶数长度:中心是两个字符之间
        const len2 = expandAroundCenter(s, i, i + 1)
        
        const len = Math.max(len1, len2)
        
        if (len > maxLen) {
            maxLen = len
            // 计算起始位置:中心位置 i 减去"半径"
            start = i - Math.floor((len - 1) / 2)
        }
    }
    
    return s.substring(start, start + maxLen)
}

function expandAroundCenter(s, left, right) {
    while (left >= 0 && right < s.length && s[left] === s[right]) {
        left--
        right++
    }
    // 循环结束时,left 和 right 已经指向了不满足条件的位置
    // 所以实际回文长度是 (left + 1) 到 (right - 1),得到(right - 1) - (left + 1) + 1 =  right - left - 1
    return right - left - 1
}

时间复杂度 O(n²),空间复杂度 O(1)。

动态规划

另一种思路是动态规划。如果我们知道 s[i+1...j-1] 是回文,且 s[i] === s[j],那么 s[i...j] 也是回文。

dp[i][j] 表示 s[i...j] 是否是回文:

  • 初始化:单个字符都是回文,dp[i][i] = true
  • 状态转移:dp[i][j] = (s[i] === s[j]) && dp[i+1][j-1]
javascript
var longestPalindrome = function(s) {
    const n = s.length
    if (n < 2) return s
    
    const dp = Array(n).fill(0).map(() => Array(n).fill(false))
    let start = 0
    let maxLen = 1
    
    // 初始化:单个字符都是回文
    for (let i = 0; i < n; i++) {
        dp[i][i] = true
    }
    
    // 从长度为 2 的子串开始
    for (let len = 2; len <= n; len++) {
        for (let i = 0; i <= n - len; i++) {
            const j = i + len - 1
            
            if (len === 2) {
                // 长度为 2:只需要检查首尾是否相等
                dp[i][j] = s[i] === s[j]
            } else {
                // 长度大于 2:首尾相等 且 中间是回文
                dp[i][j] = s[i] === s[j] && dp[i + 1][j - 1]
            }
            
            if (dp[i][j] && len > maxLen) {
                maxLen = len
                start = i
            }
        }
    }
    
    return s.substring(start, start + maxLen)
}

时间复杂度 O(n²),空间复杂度 O(n²)。

两种方法对比

方法时间复杂度空间复杂度优点缺点
中心扩展O(n²)O(1)空间效率高,代码简洁需要处理奇偶两种情况
动态规划O(n²)O(n²)思路清晰,易于理解 DP空间开销大

实际面试中,中心扩展法更常用,因为空间效率更高。

这道题在考什么?

这道题的核心是回文串的对称性中心扩展思想。回文串是中心对称的,我们可以从每个可能的中心向两边扩展,找到最长的回文。

动态规划是另一种思路,用已知的小问题的解推导出大问题的解。但在这道题中,中心扩展法更直观,空间效率也更高。

下次遇到类似题目,看到"回文"相关题目,就应该想到中心扩展或 DP。比如回文子串(统计数量)、最长回文子序列(注意是子序列)这些题,思路都是类似的。

记忆要点:

  • 回文 = 中心对称
  • 中心扩展 = 从中心向两边扩展
  • DP = 用已知的小问题解大问题