回文侦探:三种境界破解最长回文子串

回文侦探:三种境界破解最长回文子串

LeetCode 5. 最长回文子串 ------ 从暴力到线性,看懂回文问题的三种解题境界。


一、故事开场:你是回文侦探

想象你接到一个任务:在一串字符里,找出最长的"镜像文字"

比如 "babad" 里, "bab""aba" 都是回文 ------ 正着读反着读都一样。

你的目标是:在所有回文子串中,找到最长的那个。

但字符串可能有 1000 个字符,肉眼扫描?不现实。

这就是 LeetCode 第 5 题 ------ 最长回文子串。它看似简单,却藏着三种截然不同的解法境界。


二、暴力解法:为什么不直接枚举?

最笨的方法:枚举所有子串,判断是不是回文。

  • 子串数量:O(n2)O(n^2)O(n2)
  • 判断回文:O(n)O(n)O(n)
  • 总时间:O(n3)O(n^3)O(n3)

面试官听了会摇头。我们需要更聪明的方法。


三、解法一:中心扩展法(面试首选)

核心思路

回文串有个特点:它有一个中心,两边对称。

中心有两种可能:

  • 1 个字符 (奇数长度,如 "aba"
  • 2 个字符 (偶数长度,如 "abba"

所以,遍历每个字符,分别作为两种中心,向两边扩展,直到不再对称为止。

复制代码
下标: 0  1  2  3  4
字符: b  a  b  a  d
         ↑
        i=1(中心)
扩展:  左=0, 右=2 → 'b'=='b' ✅
      左=-1, 右=3 → 越界,停!
得到回文: "bab",长度 3

代码实现

java 复制代码
class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() < 1) return "";
        
        int start = 0, end = 0;
        for (int i = 0; i < s.length(); i++) {
            int len1 = expand(s, i, i);       // 奇数中心
            int len2 = expand(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 expand(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(n2)O(n^2)O(n2) ------ 每个中心最多扩展 nnn 次
  • 空间 :O(1)O(1)O(1) ------ 只记录左右边界

评价

就像一个侦探,从每个可能的中心点开始,向两边展开推理。虽然要查 nnn 个中心点,但每个案子都不复杂。


四、解法二:动态规划(理解回文的本质)

核心思路

中心扩展是"从中间向两边看",动态规划则是"从小回文推大回文"。

定义 dp[i][j]子串 s[i..j] 是否为回文串。

状态转移:

  • s[i] != s[j]dp[i][j] = false(首尾不同,肯定不是)
  • s[i] == s[j] → 看"去掉首尾后"是不是回文
    • 如果子串长度 ≤2\leq 2≤2(如 "a""aa")→ 直接为 true

    • 否则 dp[i][j] = dp[i+1][j-1]

      s = "abba"
      dp[0][3]: s[0]=='a', s[3]=='a' → 看 dp[1][2]
      dp[1][2]: s[1]=='b', s[2]=='b' → 长度=2 → true
      所以 dp[0][3] = true ✅

遍历顺序很关键: i 必须从大到小 (因为依赖 i+1),j 从小到大

代码实现

java 复制代码
class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        if (n < 2) return s;
        
        boolean[][] dp = new boolean[n][n];
        int start = 0, maxLen = 1;
        
        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 <= 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(n2)O(n^2)O(n2)
  • 空间 :O(n2)O(n^2)O(n2) ------ 需要二维数组

评价

就像建立一个"回文档案库",把每个小片段的回文性质都记录下来。当你想知道一个大片段是不是回文时,只需要查档案,而不需要重新验证。


五、解法三:马拉车算法(Manacher)------ 线性时间的奇迹

核心思路

前面两种方法都是 O(n2)O(n^2)O(n2),能不能更快?

马拉车算法做到了 O(n)O(n)O(n) 。它的核心思想是:利用回文的对称性,避免重复计算。

但直接处理奇偶长度很麻烦,所以第一步是预处理 :在每个字符间插入 #,把字符串变成统一奇数长度。

复制代码
原串:  a  b  b  a
处理:  #  a  #  b  #  b  #  a  #
下标:  0  1  2  3  4  5  6  7  8

现在所有回文都是奇数长度,中心只有一个。

算法维护一个"最右边界" right 和对应的中心 center。当处理位置 i 时:

  • 如果 iright 左边,它关于 center 的对称点 mirror 已经被算过了,可以直接利用
  • 但需要注意边界限制,不能直接照搬

代码实现

java 复制代码
class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() < 1) return "";
        
        // 预处理:插入 #,统一奇偶
        StringBuilder sb = new StringBuilder("#");
        for (char c : s.toCharArray()) {
            sb.append(c).append("#");
        }
        String t = sb.toString();
        
        int n = t.length();
        int[] p = new int[n];  // p[i] = 以 i 为中心的回文半径
        
        int center = 0, right = 0;  // 当前最右回文的中心和右边界
        int maxLen = 0, start = 0;  // 记录最长回文
        
        for (int i = 0; i < n; i++) {
            // 1. 利用对称性初始化 p[i]
            int mirror = 2 * center - i;  // i 关于 center 的对称点
            if (i < right) {
                p[i] = Math.min(right - i, p[mirror]);
            }
            
            // 2. 尝试继续扩展
            int l = i - (p[i] + 1);
            int r = i + (p[i] + 1);
            while (l >= 0 && r < n && t.charAt(l) == t.charAt(r)) {
                p[i]++;
                l--;
                r++;
            }
            
            // 3. 更新最右边界
            if (i + p[i] > right) {
                center = i;
                right = i + p[i];
            }
            
            // 4. 记录最长回文(转回原串坐标)
            if (p[i] > maxLen) {
                maxLen = p[i];
                start = (i - p[i]) / 2;
            }
        }
        
        return s.substring(start, start + maxLen);
    }
}

复杂度

  • 时间 :O(n)O(n)O(n) ------ 每个字符最多被访问常数次
  • 空间 :O(n)O(n)O(n) ------ 预处理和半径数组

评价

就像一位老练的侦探,不会每次遇到相似线索都从头推理。他会建立"对称档案":发现 A 和 B 对称,A 的结论可以直接套用到 B 上。这是算法世界里最美的"偷懒"艺术。


六、三种境界对比

境界 算法 时间 空间 核心思想 推荐指数
凡人 暴力枚举 O(n3)O(n^3)O(n3) O(1)O(1)O(1) 枚举所有子串
高手 中心扩展 O(n2)O(n^2)O(n2) O(1)O(1)O(1) 从中心向两边扩展 ⭐⭐⭐⭐⭐
大师 动态规划 O(n2)O(n^2)O(n2) O(n2)O(n^2)O(n2) 小回文推大回文 ⭐⭐⭐⭐
神仙 马拉车 O(n)O(n)O(n) O(n)O(n)O(n) 利用对称性避免重复计算 ⭐⭐⭐

选哪个?

  • 面试写代码 → 中心扩展法,代码短、空间优、不易出错
  • 理解回文本质 → 动态规划,是很多回文类题的基础
  • 追求极限性能 → 马拉车算法,但代码复杂,面试一般不考

七、写在最后

回文问题的美,在于它把"对称"这个直觉概念,变成了可以量化的算法。

从中心扩展的直观,到动态规划的系统,再到马拉车算法的优雅,三种解法像三种人生境界:

中心扩展是活在当下,动态规划是积累经验,马拉车则是站在经验的肩膀上飞翔。

但归根结底,最实用的往往是那个看起来"最笨"的中心扩展法 ------ 因为它足够简单,足够可靠,就像生活中那些朴实却有效的道理。

简单,往往是最深的功力。


欢迎在评论区分享你的理解,或者指出我表述不清的地方。一起进步!

相关推荐
heimeiyingwang1 小时前
【架构实战】Kubernetes调度器深度剖析:从Pod调度到自定义调度器
java·架构·kubernetes
Jerry2 小时前
LeetCode 108. 将有序数组转换为二叉搜索树
算法
蓝斯4972 小时前
Diff算法的简单介绍
算法
过期的秋刀鱼!2 小时前
L2正则化,防止过拟合-历史补充
算法·过拟合·l2正则化
hansang_IR3 小时前
【题解】LC:Z 算法(Z Algorithm)
c++·算法·字符串
范什么特西3 小时前
回答知识总结02
算法·哈希算法
橘子汽水1684 小时前
Leetcode 230,98:二叉搜索树中第K小的元素,验证二叉搜索树
算法·leetcode·职场和发展
Zane19944 小时前
CAS 与原子类:Java 如何实现无锁编程
java·后端
TunerT_TQ4 小时前
【智能体安全治理|专栏第9期】从“一堆规则”到“数字宪法”:智能体治理的下一个阶段
java·开发语言·安全·开源治理·大模型安全·ai基础设施·智能体安全