马拉车算法
马拉车(Manacher's Algorithm)是一种用于 求解最长回文子串 的线性时间复杂度 O(n)O(n) 算法。
📌 核心思想
- 字符串预处理:在原字符串中插入特殊字符
#
,避免奇偶回文的区分。 - 回文半径数组
P[]
:P[i]
表示以i
为中心的最长回文子串的半径。 - 回文扩展:使用 回文对称性 以及 已知信息 快速跳过不必要的计算,保持线性复杂度。
- 动态中心更新:维护
center
和right
,减少重复计算。
🚀 完整代码
public class Manacher {public static String longestPalindrome(String s) {if (s == null || s.length() == 0) {return "";}// 预处理字符串:在字符之间插入 '#'String t = preprocess(s);int n = t.length();int[] P = new int[n]; // 记录回文半径int center = 0, right = 0; // 当前回文中心和最右扩展边界int maxLen = 0, start = 0; // 记录最长回文的起始索引for (int i = 0; i < n; i++) {// 计算 P[i] 的初始值int mirror = 2 * center - i; // i 关于 center 的对称点if (i < right) {P[i] = Math.min(right - i, P[mirror]); // 利用对称性减少扩展}// 尝试中心扩展while (i + P[i] + 1 < n && i - P[i] - 1 >= 0 &&t.charAt(i + P[i] + 1) == t.charAt(i - P[i] - 1)) {P[i]++;}// 更新 center 和 rightif (i + P[i] > right) {center = i;right = i + P[i];}// 记录最长回文信息if (P[i] > maxLen) {maxLen = P[i];start = (i - maxLen) / 2; // 计算原字符串中的索引}}// 提取最长回文子串return s.substring(start, start + maxLen);}// 预处理:在字符之间插入 '#'private static String preprocess(String s) {StringBuilder sb = new StringBuilder();sb.append('#');for (char c : s.toCharArray()) {sb.append(c).append('#');}return sb.toString();}// 测试public static void main(String[] args) {String s = "babad";System.out.println("最长回文子串: " + longestPalindrome(s)); // 可能输出 "bab" 或 "aba"}
}
🔍 代码解析
- 字符串预处理
- 在每个字符之间插入
#
,将所有子串都转换为 奇数长度。 - 例如,
"babad"
→"#b#a#b#a#d#"
- 在每个字符之间插入
- 回文半径数组
P[]
P[i]
存储 以i
为中心的最大回文扩展半径。right
记录当前已扩展的最右端,center
记录当前回文中心。- 若
i
在right
之内,则可以 利用对称点mirror = 2 * center - i
的P[mirror]
直接赋值,减少计算。
- 回文扩展
- 如果
P[i]
初值已经达到边界,尝试进一步扩展: t.charAt(i + P[i] + 1) == t.charAt(i - P[i] - 1)
,向两侧扩展。
- 如果
- 更新回文中心
center
和最右边界right
- 当
i + P[i] > right
,说明回文右边界需要更新。
- 当
- 返回最长回文子串
- 由于
P[i]
是在预处理的字符串t
上计算的,我们需要 转换回原始字符串索引。
- 由于
🔹 示例
输入
String s = "babad";
预处理
# b # a # b # a # d #
计算 P[]
P = [0, 1, 0, 1, 2, 1, 4, 1, 2, 1, 0]
最长回文
可能输出 "bab"
或 "aba"
。
📌 核心循环
这个 for 循环是马拉车算法的核心部分,它用于 构造回文半径数组 P[]
,同时动态维护当前的 回文中心 center
和 最右扩展边界 right
,从而有效地 减少重复计算,保持线性时间复杂度 O(n)O(n)。
💡 变量说明
int center = 0, right = 0; // 当前回文中心 & 最右扩展边界
int maxLen = 0, start = 0; // 记录最长回文的起始索引
int[] P = new int[n]; // 记录以 i 为中心的最大回文扩展半径
P[i]
:存储以i
为中心的最大回文半径(不包括自身)。center
:当前正在扩展的回文中心索引。right
:当前回文中心的最右扩展边界(不包含)。maxLen
&start
:用于存储最长回文子串的信息。
📖 代码拆解
for (int i = 0; i < n; i++) {
遍历预处理后的字符串 t
(n
是 t.length()
,即 #b#a#b#a#d#
这样的格式)。
🟢 1️⃣ 计算 P[i]
的初始值
int mirror = 2 * center - i; // i 关于 center 的对称点
if (i < right) {P[i] = Math.min(right - i, P[mirror]); // 利用对称性减少扩展
}
🔹 解释:
mirror = 2 * center - i
计算i
关于center
的对称位置。- 如果
i
在right
之内(说明i
还在当前回文范围内),我们可以利用mirror
位置的p[mirror]
直接赋值P[i]
,减少计算:- 如果
P[mirror] < right - i
,则P[i] = P[mirror]
(对称性)。 - 否则,
P[i] = right - i
(避免越界)。
- 如果
- 目的:减少扩展的计算次数,使算法保持
O(n)
复杂度。
🟠 2️⃣ 继续扩展 P[i]
while (i + P[i] + 1 < n && i - P[i] - 1 >= 0 &&t.charAt(i + P[i] + 1) == t.charAt(i - P[i] - 1)) {P[i]++;
}
🔹 解释:
- 在
P[i]
的基础上,尝试继续向左右扩展回文子串。 - 只要:
i + P[i] + 1
不超出字符串范围。i - P[i] - 1
不超出左边界。t.charAt(i + P[i] + 1) == t.charAt(i - P[i] - 1)
(回文对称)。
- 那么
P[i]++
,继续扩展。
🟡 3️⃣ 更新 center
和 right
if (i + P[i] > right) {center = i;right = i + P[i];
}
🔹 解释:
- 如果
i + P[i]
(回文右边界)超过right
(当前已知的最右边界),说明新的回文子串更靠右,所以需要更新center
和right
。 - 这样可以使未来的
P[i]
计算尽可能复用对称回文信息。
🔴 4️⃣ 记录最长回文子串
if (P[i] > maxLen) {maxLen = P[i];start = (i - maxLen) / 2; // 计算原字符串中的索引
}
🔹 解释:
- 如果
P[i]
大于maxLen
,说明找到了更长的回文子串,更新maxLen
和start
。 start = (i - maxLen) / 2
:因为i
是预处理后的字符串中的索引,所以需要转换回原始字符串的索引。
📌 例子演示
假设 s = "babad"
,预处理后:
t = # b # a # b # a # d #
初始化:
center = 0, right = 0, P = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
📍 迭代 i = 1
mirror = 2 * 0 - 1 = -1
(无效)。right = 0
,所以P[i] = 0
。t[0] == t[2]
(都是#
),扩展P[i]++
:
P[1] = 1
- 更新
center = 1, right = 2
📍 迭代 i = 2
mirror = 0
,P[mirror] = 0
,P[2] = 0
。t[1] == t[3]
(都是b
),扩展:
P[2] = 1
t[0] == t[4]
(都是#
),继续扩展:
P[2] = 2
- 更新
center = 2, right = 4
📍 继续迭代
不断更新 P[i]
,最终找到最长回文 "bab"
或 "aba"
。
📝 复杂度分析
- 每个字符最多扩展一次,总时间复杂度是
O(n)
。
💡 总结
- 利用
mirror
对称性,减少重复计算。 - 维护
center
和right
,尽可能跳过已计算的部分。 - 保持
O(n)
复杂度,适用于超长字符串回文匹配问题。