马拉车算法(Manacher's Algorithm)讲解
马拉车算法是一种用于寻找字符串中最长回文子串的高效算法。它的核心思想是利用回文的对称性,避免不必要的重复计算,从而将时间复杂度降低到 (O(n))。
1. 问题背景
给定一个字符串 ( s ),要求找到其中最长的回文子串。回文子串是指正读和反读都相同的子串。
2. 马拉车算法的核心思想
马拉车算法通过在字符串中插入特殊字符(如 #
),将原字符串转换为一个长度为奇数的新字符串,从而简化回文中心的处理。
2.1 插入特殊字符
假设原字符串为 s
,长度为 ( n )。我们可以在每个字符之间和字符串的首尾插入特殊字符 #
,将原字符串转换为新字符串 t
,长度为 ( 2n + 1 )。
例如:
- 原字符串:
s = "abba"
- 转换后:
t = "#a#b#b#a#"
这样做的好处是,回文中心可以统一处理为一个字符,避免了奇偶长度的区分。
2.2 定义回文半径
对于新字符串 t
中的每个字符 ( t[i] ),定义 ( P[i] ) 为以 ( t[i] ) 为中心的最长回文子串的半径。例如:
- 如果 ( t[i] = 'a' ),且以 ( t[i] ) 为中心的最长回文子串为
"#a#"
,则 ( P[i] = 1 )。
2.3 利用对称性
假设我们已经计算了 ( P[i] ),并且知道以 ( t[i] ) 为中心的最长回文子串。当计算 ( P[j] ) 时,如果 ( j ) 在以 ( t[i] ) 为中心的回文子串内,可以通过对称性快速计算 ( P[j] )。
具体来说:
- 如果 ( j ) 在以 ( t[i] ) 为中心的回文子串内,且 ( j ) 关于 ( i ) 的对称位置为 ( j' ),则 ( P[j] ) 至少为 ( P[j'] )。
- 如果 ( j + P[j'] ) 超出了以 ( t[i] ) 为中心的回文子串范围,则需要进一步扩展 ( P[j] )。
3. 算法步骤
- 预处理字符串 :将原字符串
s
转换为新字符串t
,并在每个字符之间插入#
。 - 初始化数组:定义数组 ( P ),其中 ( P[i] ) 表示以 ( t[i] ) 为中心的最长回文子串的半径。
- 遍历字符串 :
- 维护两个变量:
center
和right
,分别表示当前已知的最长回文子串的中心和右边界。 - 对于每个位置 ( i ):
- 如果 ( i ) 在
right
的范围内,计算P[i]
的初始值: P[i]=min(P[2×center−i],right−i) - 如果 ( i ) 超出
right
的范围,初始化P[i] = 0
。 - 从 ( i + P[i] + 1 ) 开始扩展,直到不满足回文条件。
- 更新
P[i]
。 - 如果 ( i + P[i] > right ),更新
center = i
和right = i + P[i]
。
- 如果 ( i ) 在
- 维护两个变量:
- 计算最长回文子串 :遍历
P
数组,找到最大值及其位置,从而确定最长回文子串。
4. JavaScript 代码实现
javascript
function longestPalindromicSubstring(s) {
// 预处理字符串
const t = `#${s.split('').join('#')}#`;
const n = t.length;
const P = new Array(n).fill(0);
let center = 0, right = 0;
let maxLen = 0, centerIndex = 0;
for (let i = 0; i < n; i++) {
// 如果当前字符在已知的最长回文子串范围内,利用对称性计算初始 P[i]
if (i < right) {
const mirror = 2 * center - i;
P[i] = Math.min(P[mirror], right - i);
}
// 尝试扩展回文子串
while (i - P[i] - 1 >= 0 && i + P[i] + 1 < n && t[i - P[i] - 1] === t[i + P[i] + 1]) {
P[i]++;
}
// 如果扩展后的回文子串超出了已知的右边界,更新中心和右边界
if (i + P[i] > right) {
center = i;
right = i + P[i];
}
// 更新最长回文子串的信息
if (P[i] > maxLen) {
maxLen = P[i];
centerIndex = i;
}
}
// 提取最长回文子串
const start = (centerIndex - maxLen) >>> 1;
return s.substring(start, start + maxLen);
}
// 示例
const s = "babad";
console.log(longestPalindromicSubstring(s)); // 输出 "bab" 或 "aba"
代码说明
- 预处理字符串 :通过在每个字符之间插入
#
,将原字符串转换为新字符串t
。 - 初始化数组 :
P
数组用于存储每个位置的回文半径。 - 遍历字符串 :
- 使用
center
和right
维护当前已知的最长回文子串的中心和右边界。 - 对于每个位置
i
,根据是否在right
范围内,计算P[i]
的初始值。 - 通过扩展回文子串更新
P[i]
,并根据需要更新center
和right
。
- 使用
- 提取结果 :根据
P
数组找到最长回文子串的中心和长度,提取对应的子串。
时间复杂度分析
马拉车算法的时间复杂度为 (O(n)),其中 ( n ) 是新字符串 t
的长度。这是因为每个字符最多被扩展一次,且每次扩展操作的时间复杂度为 (O(1))。
个人总结
通过定义两个重要变量,回文中心位置以及回文半径,进行遍历计算。 优化计算的关键是运用上之前计算值,因此在遍历回文中心时,想办法用上之前的回文中心计算结果,找到两者之间关系(即参照之前某次回文中心位置和回文半径,是否能够直接求出当前回文中心的回文半径),进行优化。