最长回文子串 - 中心扩展法 & 马拉车算法 (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)长度计算易错点
退出循环后,left 和 right 已超出回文边界,因此:
真实回文区间是 [left+1, right-1],长度计算公式为:
长度=(right−1)−(left+1)+1=right−left−1长度 = (right-1) - (left+1) + 1 = right - left - 1长度=(right−1)−(left+1)+1=right−left−1
示例验证:
回文 "bb"(原字符串索引1-2),扩展退出时 left=0,right=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)) 优化算法,核心解决两个问题:
- 中心扩展法需分别处理奇偶长度回文的问题;
- 中心扩展法重复计算同一字符的回文半径的问题。
核心优化思路
| 优化点 | 具体做法 |
|---|---|
| 统一奇偶回文 | 插入特殊字符(如#),将所有回文转为奇数长度(如"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" 为例:
- 预处理:
"cbbd"→T = "#c#b#b#d#"(长度9); - 初始化:
P = [0,0,0,0,0,0,0,0,0],center=0,right=0,maxLen=0,maxCenter=0; - 遍历
i=0到8:i=3(T[3] = b):i < right不成立,P[3]=0,扩展后P[3]=1,right=4,center=3;i=4(T[4] = #):i < right成立,mirror=2,P[4] = min(4-4, P[2])=0,扩展后P[4]=2,right=6,center=4,maxLen=2,maxCenter=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 - i:i到当前最长回文右边界的距离(最多只能扩展到right);P[mirror]:对称点的回文半径(若超过right - i,则无法复用,需从right - i开始扩展)。
示例:T = #c#b#b#d#,center=4,right=6,i=5:
mirror = 2*4 -5 = 3;right - i = 6-5=1,P[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=4,maxLen=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)=16 | O(4)=4O(4)=4O(4)=4 | 小字符串差异小 |
"aaaaa...a"(1000个a) | O(10002)=106O(1000^2)=10^6O(10002)=106 | O(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方法特性)。

451

被折叠的 条评论
为什么被折叠?



