最长回文子串 (中心扩展法 | Manacher马拉车线性优化)

最长回文子串 - 中心扩展法 & 马拉车算法 (Java)

一、核心背景与问题定义

1. 问题来源

LeetCode 最长回文子串
题目要求:
给定一个字符串 s,找到 s 中最长的回文子串。回文子串指正读和反读完全相同的连续子串(如 "bab""bb")。

2. 基础概念

  • 回文子串:连续字符序列,对称分布(如 "abcba" 是奇数长度回文,"abba" 是偶数长度回文);
  • 子串 vs 子序列:子串是连续字符,子序列可非连续(本题仅关注子串);
  • 核心目标:在 O(n2)O(n^2)O(n2) 或更优时间复杂度内找到最长回文子串。

二、解法一:中心扩展法(O(n2)O(n^2)O(n2)

1. 算法核心原理

回文的本质是轴对称,中心扩展法通过遍历所有可能的「回文中心」,向左右两侧扩展验证对称性,最终记录最长回文。

回文中心的两种类型
回文长度中心形式示例中心位置
奇数单个字符(对称轴过字符)"aba"索引 1(字符 b
偶数两个字符间隙(对称轴过间隙)"abba"索引 1-2 之间
算法执行流程(可视化)

s = "cbbd" 为例:

遍历i=0(字符c):
- 奇数中心:扩展后仅c自身,长度1;
- 偶数中心:c和b不相等,无扩展;
遍历i=1(字符b):
- 奇数中心:b自身,长度1;
- 偶数中心:b(i=1)和b(i=2)相等,扩展后长度2("bb");
遍历i=2(字符b):
- 奇数中心:b自身,长度1;
- 偶数中心:b和d不相等,无扩展;
遍历i=3(字符d):
- 奇数中心:d自身,长度1;
- 偶数中心:无(越界);
最终最长回文为"bb"。

2. 完整Java代码(逐行注释版)

class Solution {
    // 全局变量:记录最长回文子串的起始索引
    private int startIndex;
    // 全局变量:记录最长回文子串的长度
    private int maxPalindromeLength;

    public String longestPalindrome(String s) {
        // 边界条件1:字符串为空或null,直接返回空串
        if (s == null || s.isEmpty()) {
            return "";
        }
        // 边界条件2:字符串长度为1,自身就是最长回文
        if (s.length() == 1) {
            return s;
        }

        // 字符串总长度
        int strLength = s.length();
        
        // 遍历每个字符,作为回文中心(覆盖奇数/偶数长度)
        for (int i = 0; i < strLength; i++) {
            // 处理奇数长度回文:中心为s[i]
            expandFromCenter(s, i, i);
            // 处理偶数长度回文:中心为s[i]和s[i+1]之间的间隙
            expandFromCenter(s, i, i + 1);
        }

        // 截取最长回文子串:substring(起始索引, 结束索引)(左闭右开)
        // 结束索引 = 起始索引 + 长度
        return s.substring(startIndex, startIndex + maxPalindromeLength);
    }

    /**
     * 中心扩展核心方法:从指定左右边界向两侧扩展,验证回文
     * @param s 原字符串
     * @param left 扩展起始左边界
     * @param right 扩展起始右边界
     */
    private void expandFromCenter(String s, int left, int right) {
        // 扩展条件:
        // 1. left >= 0:左边界不越界
        // 2. right < s.length():右边界不越界
        // 3. s.charAt(left) == s.charAt(right):左右字符相等(满足回文)
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            // 左边界向左移动一位
            left--;
            // 右边界向右移动一位
            right++;
        }

        // 退出循环时的状态:
        // left/right已超出回文边界(要么越界,要么字符不相等)
        // 例如:回文为"bb",退出时left=0,right=3,实际回文区间是[1,2]
        // 计算当前回文的真实长度:(right - 1) - (left + 1) + 1 = right - left - 1
        int currentLength = right - left - 1;

        // 如果当前回文长度大于已记录的最长长度,更新全局变量
        if (currentLength > maxPalindromeLength) {
            // 更新最长长度
            maxPalindromeLength = currentLength;
            // 更新起始索引:left多减了1,所以真实起始是left + 1
            startIndex = left + 1;
        }
    }
}

3. 关键细节拆解

(1)变量作用详解
变量名作用
startIndex记录最长回文子串在原字符串中的起始索引(全局变量避免方法传参繁琐)
maxPalindromeLength记录最长回文子串的长度(全局变量)
left/right扩展的临时边界,初始为回文中心,逐步向两侧移动
currentLength单次扩展得到的回文长度,需修正越界后的偏差
(2)长度计算易错点

退出循环后,leftright 已超出回文边界,因此:
真实回文区间是 [left+1, right-1],长度计算公式为:
长度=(right−1)−(left+1)+1=right−left−1长度 = (right-1) - (left+1) + 1 = right - left - 1长度=(right1)(left+1)+1=rightleft1

示例验证:
回文 "bb"(原字符串索引1-2),扩展退出时 left=0right=3,长度 = 3 - 0 - 1 = 2(正确)。

4. 复杂度分析

时间复杂度

O(n2)O(n^2)O(n2),其中 nnn 是字符串长度:

  • 遍历每个字符(nnn 次);
  • 每个字符最多向两侧扩展 nnn 次(最坏情况如 "aaaaa");
  • 总操作数:n×n=n2n \times n = n^2n×n=n2
空间复杂度

O(1)O(1)O(1),仅使用常量级额外空间(无数组/集合,仅几个变量)。

5. 测试用例执行结果

输入字符串输出结果执行过程关键信息
"babad""bab"最长回文中心为索引1(字符b),长度3
"cbbd""bb"最长回文中心为索引1-2,长度2
"a""a"直接触发边界条件返回
"ac""a"无偶数长度回文,最长为单个字符

三、解法二:马拉车算法(O(n)O(n)O(n)

1. 算法核心原理

马拉车算法(Manacher’s Algorithm)是专门针对「最长回文子串」的线性时间(O(n)O(n)O(n) 优化算法,核心解决两个问题:

  1. 中心扩展法需分别处理奇偶长度回文的问题;
  2. 中心扩展法重复计算同一字符的回文半径的问题。
核心优化思路
优化点具体做法
统一奇偶回文插入特殊字符(如#),将所有回文转为奇数长度(如"cbbd""#c#b#b#d#"
复用对称回文信息记录当前最长回文的中心(center)和右边界(right),利用对称点复用回文半径
关键概念定义
概念具体含义
预处理字符串 T原字符串插入#后的字符串(如"abc""#a#b#c#"
P[] 数组P[i] 表示以 T[i] 为中心的最长回文子串的半径(包含中心自身)
center当前已知的最长回文子串的中心索引(针对T
right当前已知的最长回文子串的右边界索引(针对T,开区间)
mirror位置i关于center的对称点,计算公式:mirror = 2 * center - i
预处理字符串示例
原字符串预处理后字符串 T原回文长度预处理后回文长度说明
"aba""#a#b#a#"3(奇数)7(奇数)统一为奇数长度
"abba""#a#b#b#a#"4(偶数)9(奇数)统一为奇数长度

2. 算法执行流程

s = "cbbd" 为例:

  1. 预处理:"cbbd"T = "#c#b#b#d#"(长度9);
  2. 初始化:P = [0,0,0,0,0,0,0,0,0]center=0right=0maxLen=0maxCenter=0
  3. 遍历i=08
    • i=3T[3] = b):i < right不成立,P[3]=0,扩展后P[3]=1right=4center=3
    • i=4T[4] = #):i < right成立,mirror=2P[4] = min(4-4, P[2])=0,扩展后P[4]=2right=6center=4maxLen=2maxCenter=4
  4. 还原结果:start = (4 - 2)/2 = 1,截取s[1,1+2]"bb"

3. 完整Java代码

class Solution {
    public String longestPalindrome(String s) {
        // ========== 步骤1:边界条件处理 ==========
        // 空字符串直接返回
        if (s == null || s.isEmpty()) {
            return "";
        }
        // 长度为1的字符串直接返回自身(减少不必要的预处理)
        if (s.length() == 1) {
            return s;
        }

        // ========== 步骤2:预处理字符串,统一奇偶回文 ==========
        StringBuilder sb = new StringBuilder();
        // 开头插入#(确保第一个字符前有分隔符)
        sb.append('#');
        // 遍历原字符串,每个字符后插入#
        for (char c : s.toCharArray()) {
            sb.append(c);
            sb.append('#');
        }
        // 预处理后的字符串T(如"cbbd"→"#c#b#b#d#")
        String T = sb.toString();
        // 预处理后字符串的长度
        int tLength = T.length();

        // ========== 步骤3:初始化核心变量 ==========
        // P[i]:以T[i]为中心的回文半径(包含中心)
        int[] P = new int[tLength];
        // center:当前最长回文的中心(针对T)
        int center = 0;
        // right:当前最长回文的右边界(针对T,开区间,即right位置不属于回文)
        int right = 0;
        // maxLen:全局最长回文的半径(针对T)
        int maxLen = 0;
        // maxCenter:全局最长回文的中心索引(针对T)
        int maxCenter = 0;

        // ========== 步骤4:遍历预处理字符串,计算P数组 ==========
        for (int i = 0; i < tLength; i++) {
            // 子步骤4.1:利用对称性快速初始化P[i],避免重复计算
            if (i < right) {
                // 计算i关于center的对称点mirror
                int mirror = 2 * center - i;
                // 核心优化:P[i]的初始值取较小值(避免超出right边界)
                // right - i:i到右边界的距离(最多扩展到right)
                // P[mirror]:对称点的回文半径
                P[i] = Math.min(right - i, P[mirror]);
            }
            // 注:若i >= right,P[i]默认值为0(数组初始化时已为0)

            // 子步骤4.2:向左右扩展,更新P[i](显式边界更易读)
            // 左边界:i - (当前半径 + 1) → 尝试扩展的下一个左位置
            int leftBound = i - (P[i] + 1);
            // 右边界:i + (当前半径 + 1) → 尝试扩展的下一个右位置
            int rightBound = i + (P[i] + 1);
            // 扩展条件:边界不越界 + 字符相等
            while (leftBound >= 0 && rightBound < tLength && T.charAt(leftBound) == T.charAt(rightBound)) {
                // 半径+1
                P[i]++;
                // 左边界继续左移
                leftBound--;
                // 右边界继续右移
                rightBound++;
            }

            // 子步骤4.3:更新当前最长回文的center和right
            // 若当前回文的右边界(i + P[i])超出已知right,更新中心和边界
            if (i + P[i] > right) {
                center = i;
                right = i + P[i];
            }

            // 子步骤4.4:更新全局最长回文信息
            if (P[i] > maxLen) {
                maxLen = P[i];       // 更新最长半径
                maxCenter = i;       // 更新最长回文中心
            }
        }

        // ========== 步骤5:还原原字符串的最长回文子串 ==========
        // 核心公式:
        // 原字符串起始索引 = (预处理中心索引 - 预处理半径) / 2
        int start = (maxCenter - maxLen) / 2;
        // 截取:起始索引 ~ 起始索引+长度(maxLen即为原回文长度)
        return s.substring(start, start + maxLen);
    }
}

4. 关键细节拆解

(1)预处理的核心作用
原字符串问题预处理解决方式示例
奇偶长度回文需分别处理插入#统一为奇数长度"abba"(偶数)→#a#b#b#a#(奇数)
扩展时边界判断复杂预处理后扩展逻辑统一所有回文的中心都是单个字符(#或原字符)
(2)对称复用的核心逻辑

i < right 时,P[i] 的初始值取 Math.min(right - i, P[mirror]),原因:

  • right - ii 到当前最长回文右边界的距离(最多只能扩展到right);
  • P[mirror]:对称点的回文半径(若超过right - i,则无法复用,需从right - i开始扩展)。

示例:T = #c#b#b#d#center=4right=6i=5

  • mirror = 2*4 -5 = 3
  • right - i = 6-5=1P[3]=1
  • P[5] = min(1,1)=1,无需重复计算对称点的半径。
(3)结果还原公式推导

预处理后回文半径 maxLen 与原字符串回文长度的关系:

  • 预处理后回文长度 = 2 * maxLen + 1(半径包含中心,总长度=2*半径+1);
  • 原字符串回文长度 = maxLen(预处理插入的#不占原长度);
  • 原字符串起始索引 = (maxCenter - maxLen) / 2(预处理索引与原索引的映射关系)。

示例验证:

  • T = #c#b#b#d#maxCenter=4maxLen=2
  • 原起始索引 = (4-2)/2 = 1,原长度=2 → 截取s[1,3]"bb"(正确)。

5. 复杂度分析

时间复杂度

O(n)O(n)O(n),其中 nnn 是原字符串长度:

  • 预处理字符串长度为 2n+12n+12n+1
  • 每个字符最多被扩展一次(leftBound/rightBound 只向右移动,无回退);
  • 总操作数与预处理字符串长度成正比,即 O(n)O(n)O(n)
空间复杂度

O(n)O(n)O(n),主要消耗:

  • P[] 数组:长度为 2n+12n+12n+1
  • 预处理字符串 T:长度为 2n+12n+12n+1

6. 与中心扩展法的对比测试

测试用例中心扩展法耗时(近似)马拉车算法耗时(近似)说明
"a"O(1)O(1)O(1)O(1)O(1)O(1)边界场景无差异
"cbbd"O(42)=16O(4^2)=16O(42)=16O(4)=4O(4)=4O(4)=4小字符串差异小
"aaaaa...a"(1000个a)O(10002)=106O(1000^2)=10^6O(10002)=106O(1000)=103O(1000)=10^3O(1000)=103长字符串效率提升1000倍

四、两种算法全面对比

核心维度对比

维度中心扩展法马拉车算法
时间复杂度O(n2)O(n^2)O(n2)O(n)O(n)O(n)
空间复杂度O(1)O(1)O(1)(最优)O(n)O(n)O(n)(额外数组/字符串)
实现难度低(新手友好)中(需理解对称复用)
可读性极高(逻辑直观)较高(优化后)
适用场景短字符串(n≤1000)长字符串(n>1000,追求效率)

五、核心知识点总结

1. 中心扩展法核心

  • 回文有两种中心(单字符/双字符间隙),需遍历所有中心;
  • 扩展后需修正边界偏差,真实长度= right - left - 1
  • 空间复杂度最优,适合短字符串场景。

2. 马拉车算法核心

  • 预处理插入#统一奇偶回文,简化扩展逻辑;
  • 利用center/right/mirror复用对称回文半径,实现线性时间;
  • 结果还原公式:原起始索引= (maxCenter - maxLen) / 2,原长度= maxLen

3. 通用注意事项

  • LeetCode提交需保证类名为Solution,边界条件覆盖空串/长度1;
  • 回文子串是连续的,需区分于回文子序列;
  • 计算回文长度时需注意「左闭右开」的截取规则(substring方法特性)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值