【每日算法】LeetCode 5. 最长回文子串(动态规划)

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。

------ 算法:资深前端开发者的进阶引擎

LeetCode 5. 最长回文子串

1. 题目描述

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

示例 1:

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

示例 2:

复制代码
输入:s = "cbbd"
输出:"bb"

提示:

  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成

2. 问题分析

回文串是一个正读和反读都一样的字符串(如"上海自来水来自海上")。在前端开发中,处理字符串的场景无处不在:用户输入校验、数据格式化、富文本编辑器功能实现(如查找、高亮)等。虽然直接寻找最长回文子串的业务场景不常见,但解决此问题所运用的中心扩展动态规划思想,以及对于字符串操作性能的敏感度,对于构建高效、复杂的前端应用至关重要。例如,在实现一个实时语法高亮或差异对比(diff)功能时,类似的子串处理逻辑是核心。

3. 解题思路

解决"最长回文子串"问题,主要有三种经典思路:

  1. 暴力枚举 (Brute Force):枚举所有子串,判断是否为回文。时间复杂度为 O(n³),在 LeetCode 数据规模下必然超时,不实用。
  2. 动态规划 (Dynamic Programming):利用"一个回文串去掉头尾后仍然是回文串"这一特性,用空间换时间,将时间复杂度降为 O(n²),空间复杂度也为 O(n²)。
  3. 中心扩散法 (Expand Around Center):回文串具有对称性。我们可以遍历每一个可能的"中心",尝试向两边扩展,直到无法形成回文为止。这是该问题的最优常规解法,时间复杂度 O(n²),空间复杂度 O(1)。
  4. Manacher 算法:一种专门的线性时间 O(n) 算法,但理解和实现较复杂,在前端面试中通常不作要求。

最优解推荐 :对于前端面试和日常工程,中心扩散法在效率、实现复杂度和可读性上达到了最佳平衡,是必须掌握的核心解法。

4. 各思路代码实现 (JavaScript)

4.1 中心扩散法 (最优解)

javascript 复制代码
/**
 * @param {string} s
 * @return {string}
 */
const longestPalindrome = function(s) {
    if (s.length < 2) return s;

    let start = 0; // 记录最长回文子串的起始索引
    let maxLen = 1; // 记录最长回文子串的长度(初始为1,单个字符是回文)

    /**
     * 中心扩散辅助函数
     * @param {number} left - 中心左边界
     * @param {number} right - 中心右边界
     * @returns {number} 扩散后回文串的长度
     */
    const expandAroundCenter = (left, right) => {
        // 当左右指针在边界内,且所指字符相等时,向两边扩展
        while (left >= 0 && right < s.length && s[left] === s[right]) {
            left--;
            right++;
        }
        // 循环结束时,s[left] !== s[right] 或越界
        // 因此实际回文串的索引范围是 [left + 1, right - 1]
        // 回文串长度 = (right - 1) - (left + 1) + 1 = right - left - 1
        return right - left - 1;
    };

    for (let i = 0; i < s.length; i++) {
        // 情况1:以单个字符 s[i] 为中心(奇数长度回文串,如 "aba")
        const len1 = expandAroundCenter(i, i);
        // 情况2:以相邻字符 s[i] 和 s[i+1] 的"间隙"为中心(偶数长度回文串,如 "abba")
        const len2 = expandAroundCenter(i, i + 1);

        // 获取本次循环中两种中心扩散出的较长回文串长度
        const currentMaxLen = Math.max(len1, len2);

        // 如果当前长度大于历史最大长度,则更新起始位置和最大长度
        if (currentMaxLen > maxLen) {
            maxLen = currentMaxLen;
            // 核心:根据中心和长度,反推回文串的起始索引
            // start = i - Math.floor((maxLen - 1) / 2)
            start = i - Math.floor((maxLen - 1) / 2);
        }
    }
    // 根据起始索引和最大长度,截取并返回最长回文子串
    return s.substring(start, start + maxLen);
};

// 示例步骤分解:以 s = "babad" 为例,i=1时 (字符 'a')
// len1 = expandAroundCenter(1,1): 中心为'a'
//  初始: left=1, right=1 -> s[1]='a' == s[1]='a'
//  扩展: left=0 ('b'), right=2 ('b') -> 相等
//  再扩: left=-1, right=3 -> 越界,停止。长度 = 3 - (-1) - 1 = 3
// len2 = expandAroundCenter(1,2): 中心为'a'和'b'之间
//  初始: left=1('a'), right=2('b') -> 不相等,直接停止。长度 = 2 - 1 - 1 = 0
// currentMaxLen = 3 > maxLen(1) -> 更新 maxLen=3, start = 1 - Math.floor((3-1)/2) = 0
// 最终返回 s.substring(0, 0+3) = "bab"

4.2 动态规划法

javascript 复制代码
/**
 * @param {string} s
 * @return {string}
 */
const longestPalindromeDP = function(s) {
    const n = s.length;
    if (n < 2) return s;

    // 1. 定义状态:dp[i][j] 表示子串 s[i..j] 是否为回文串
    // 使用二维数组记录,空间复杂度 O(n²)
    const dp = new Array(n).fill(false).map(() => new Array(n).fill(false));
    let start = 0;
    let maxLen = 1;

    // 2. 初始化:所有长度为1的子串都是回文串
    for (let i = 0; i < n; i++) {
        dp[i][i] = true;
    }

    // 3. 状态转移:枚举子串长度 len,从2开始到n
    for (let len = 2; len <= n; len++) {
        // 枚举左边界 i
        for (let i = 0; i <= n - len; i++) {
            // 根据左边界和长度确定右边界 j
            const j = i + len - 1;

            // 核心状态转移方程:
            // dp[i][j] 为 true 当且仅当:
            //  1. 首尾字符相等 (s[i] === s[j])
            //  2. 并且去掉首尾后的子串是回文串 (dp[i+1][j-1] 为 true)
            //  或者,子串长度仅为2 (len === 2) 时,只需判断首尾字符
            if (s[i] === s[j]) {
                if (len === 2 || dp[i + 1][j - 1]) {
                    dp[i][j] = true;
                    // 如果当前回文串更长,则更新结果
                    if (len > maxLen) {
                        maxLen = len;
                        start = i;
                    }
                }
            } // 如果首尾字符不相等,dp[i][j] 默认为 false,无需操作
        }
    }

    return s.substring(start, start + maxLen);
};

// 状态转移示例 (s="babad"),理解 dp 表的填充顺序:
// 先填 len=1 的对角线: dp[0][0], dp[1][1]... = true
// len=2: 判断所有相邻字符
//   i=0,j=1: s[0]='b', s[1]='a' -> 不相等,dp[0][1]=false
//   i=1,j=2: 'a' vs 'b' -> false
//   i=2,j=3: 'b' vs 'a' -> false
//   i=3,j=4: 'a' vs 'd' -> false
// len=3: 判断长度为3的子串
//   i=0,j=2: s[0]='b', s[2]='b' 相等,且 len!=2, 看 dp[1][1]=true -> 所以 dp[0][2]=true -> 更新结果 start=0, maxLen=3
//  ...以此类推。这种方法逻辑清晰,但空间占用大。

5. 各实现思路的复杂度、优缺点对比

方法 时间复杂度 空间复杂度 优点 缺点 适用场景
中心扩散法 O(n²) O(1) 1. 空间效率极高。 2. 思路直观,代码相对简洁。 3. 性能稳定,是最优常规解。 1. 理论时间复杂度不是最优(但已足够)。 前端面试首选、大多数字符串回文问题、对内存敏感的环境。
动态规划 O(n²) O(n²) 1. 思路具有通用性,是许多字符串问题的模板。 2. 状态定义清晰,易于理解和推导。 1. 空间占用大,在 n 较大时可能成为瓶颈。 2. 代码实现稍显繁琐。 适用于需要记录所有子串状态的更复杂问题(如分割回文串II)。
Manacher算法 O(n) O(n) 1. 理论时间复杂度最优。 1. 算法复杂,不易理解和记忆。 2. 实现容易出错。 3. 在前端面试中不常见。 对性能有极致要求的后端服务或算法竞赛。

6. 总结

6.1 通用解题模板/思路

对于回文类问题,可以遵循以下思考路径:

  1. 识别特征 :牢记回文的对称性。无论是从中心扩散,还是动态规划中"大问题依赖于小问题"的特性,都源于此。

  2. 优先考虑中心扩散法 :对于"最长回文子串"、"回文子串个数"等问题,中心扩散法通常是最高效、最清晰的实现方式。其核心模板如下:

    javascript 复制代码
    function expand(left, right) {
        while (满足边界条件 && 左右字符相等) {
            left--; right++;
        }
        return 回文长度或子串;
    }
    for (遍历每个索引作为中心) {
        处理奇数长度情况(expand(i, i));
        处理偶数长度情况(expand(i, i+1));
        更新最优结果;
    }
  3. 考虑动态规划 :当问题要求的结果不仅仅是"一个",或者需要记录中间所有状态时(例如"分割回文串"需要知道所有子串是否回文),动态规划是更好的选择。其状态定义通常为 dp[i][j] 表示子串 s[i..j] 是否回文。

  4. 前端关联思考:将"中心"视为UI组件的"状态",将"扩散"视为"副作用"或"更新"。理解如何高效地更新和传播状态,是构建复杂前端应用(如状态管理、响应式更新)的核心能力。

6.2 类似题目推荐

  • LeetCode 647. 回文子串:计算字符串中有多少个回文子串。中心扩散法的直接应用。
  • LeetCode 516. 最长回文子序列 :求最长回文子序列的长度(子序列可以不连续)。这是动态规划的经典题目,状态定义与本题类似但转移方程不同。
  • LeetCode 131. 分割回文串 / 132. 分割回文串 II:需要先利用动态规划预处理,得到所有子串的回文信息,再进行深度优先搜索或二次动态规划。是综合应用的好题目。
相关推荐
老赵聊算法、大模型备案2 小时前
《人工智能拟人化互动服务管理暂行办法(征求意见稿)》深度解读:AI“拟人”时代迎来首个专项监管框架
人工智能·算法·安全·aigc
雪花desu2 小时前
【Hot100-Java中等】/LeetCode 128. 最长连续序列:如何打破排序思维,实现 O(N) 复杂度?
数据结构·算法·排序算法
松涛和鸣2 小时前
41、Linux 网络编程并发模型总结(select / epoll / fork / pthread)
linux·服务器·网络·网络协议·tcp/ip·算法
鹿角片ljp2 小时前
力扣26.有序数组去重:HashSet vs 双指针法
java·算法
XFF不秃头2 小时前
力扣刷题笔记-合并区间
c++·笔记·算法·leetcode
巧克力味的桃子3 小时前
学习笔记:查找数组第K小的数(去重排名)
笔记·学习·算法
星云POLOAPI3 小时前
大模型API调用延迟过高?深度解析影响首Token时间的五大因素及优化方案
人工智能·python·算法·ai
88号技师3 小时前
2026年1月一区SCI-波动光学优化算法Wave Optics Optimizer-附Matlab免费代码
开发语言·算法·数学建模·matlab·优化算法
程序员阿鹏3 小时前
如何保证写入Redis的数据不重复
java·开发语言·数据结构·数据库·redis·缓存