前言
学习算法之前,我们先理清楚几个基础概念:
回文串:正读和反读一样的串
回文子串:原文的某个回文串
最长回文子串:原串中最长的回文子串
接下来,我们看看问题是什么:
给你一个字符串 s,找到 s 中最长的 回文 子串。

接下来我一共会讲解三种算法。
暴力算法(太落后,可以略过不看😄)
这个是人们普遍最容易想到和理解的算法,思路就是枚举所有可能的子串,然后逐一验证是否回文,保留最长的那个。
用两层 for 循环枚举所有子串的起始 i 和结束 j 下标。
对每个子串 s[i...j] ,调用 isPalindrome 函数检查它是否是回文。
如果是回文且长度大于当前记录的最大值,就更新最大长度和起始位置。
最后通过起始位置和最大长度截取并返回最长回文子串。
代码:
cpp
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n < 2) return s;
int maxLen = 1;
int start = 0;
// 枚举所有子串
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (isPalindrome(s, i, j) && j - i + 1 > maxLen) {
maxLen = j - i + 1;
start = i;
}
}
}
return s.substr(start, maxLen);
}
private:
bool isPalindrome(const string& s, int l, int r) {
while (l < r) {
if (s[l] != s[r]) return false;
l++;
r--;
}
return true;
}
};
这方法的优点就是逻辑直观容易理解,不需要额外处理奇偶回文,枚举自然覆盖所有情况,缺点就非常的明显了,时间复杂度高达O(n³),在长度较长时,会超时,不适合实际工程和面试场景。
中心扩展法
先说说大致思路:利用回文的中心对称特性,枚举所有可能的回文中心,然后向两边扩展。
回文中心分两种:
- 奇数长度回文:中心是单个字符,对应 expandAroundCenter(s, i, i)
- 偶数长度回文:中心是两个字符的间隙,对应 expandAroundCenter(s, i, i+1)
对每个中心,向左右扩展,直到字符不相等或越界,扩展结束后,得到当前中心能扩展出的最长回文子串的左右边界,比较所有回文子串的长度,保留最长的那个的起始位置和长度,最后截取子串返回。
代码:
cpp
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n < 2) return s;
int start = 0, maxLen = 1;
for (int i = 0; i < n; ++i) {
// 奇数长度回文
auto [l1, r1] = expandAroundCenter(s, i, i);
// 偶数长度回文
auto [l2, r2] = expandAroundCenter(s, i, i + 1);
if (r1 - l1 + 1 > maxLen) {
maxLen = r1 - l1 + 1;
start = l1;
}
if (r2 - l2 + 1 > maxLen) {
maxLen = r2 - l2 + 1;
start = l2;
}
}
return s.substr(start, maxLen);
}
private:
pair<int, int> expandAroundCenter(const string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
left--;
right++;
}
return {left + 1, right - 1};
}
};
中心扩展法的优点是时间复杂度为O(n²)比暴力算法高一个等级,缺点就是要显示处理奇偶两种回文,下面要讲述的算法碾压这两种算法。
Manacher【马拉车算法】(重点)
马拉车算法是专门用来解决最长回文子串问题的线性时间复杂度O(n)算法,它通过预处理字符串和维护回文半径的方式,避免了中心扩展法的重复计算,是目前最高效的解法。
1.预处理字符串-------统一字符串的奇偶
中心扩展法回文的奇偶长度会增加处理的复杂性,马拉车算法的第一步就是统一奇偶长度:
在原字符串的每个字符之间和首尾插入一个特殊符号(如 # ),比如 "babad" 会被处理成 "#b#a#b#a#d#" 。
这样,原字符串中的所有奇偶长度回文,在新字符串中都变成了奇数长度的回文,只需统一处理一种情况即可。
这里有个技巧:
首尾加不同的哨兵符(如 ^ 和 $ ),扩展时无需判断下标越界(哨兵符永远不匹配,自动终止),简化代码。
2.搞清楚三个比较核心的东西
所有概念基于预处理后的字符串 t (如 ^#b#a#d#$ ):
1.回文半径数组p[ ]
p[i] :以 t[i] 为中心的最长回文半径(包含中心本身)。
关键换算:原串回文长度 = p[i] - 1(抵消 # 的影响)。
比如:
t=#a#b#a# ,中心 b 的 p[i]=4 → 原串回文长度=4-1=3(即 aba ),完全匹配。
2.最右回文边界 R
遍历中所有已找到的回文,最靠右的右边界下标(行业约定:开区间,实际右边界是 R-1 )。
作用:划分「已探索区域(i < R)」和「未探索区域(i ≥ R)」,已探索区可复用对称性,未探索区才暴力扩展。
3.最右回文中心 C
对应 R 的那个回文的中心下标,和 R 绑定更新(只有新回文的右边界超过 R ,才同步更新 C 和 R )。
作用:找当前位置 i 的对称点 i_mirror = 2*C - i (数轴对称公式),复用 i_mirror 的回文信息,避免重复计算。
搞清楚这些,下面我将详细讲解马拉车算法的逻辑步骤
3.四步核心算法处理
遍历预处理后的字符串 t (跳过首尾哨兵符),对每个位置 i ,只做4件事:
步骤1:利用对称性初始化 p[i] (减少重复计算)
根据 i 和 R 的位置,分2种情况,只初始化,不扩展:
- i < R(已探索区): p[i] = min(R - i, p[2C - i]) ( 2C - i 是对称点, R-i 是i在已探索区的最大可复用半径,取小值避免越界)。
- i ≥ R(未探索区): p[i] = 1 (最小半径,仅包含中心本身,后续暴力扩展)。
步骤2:暴力扩展更新 p[i] (仅扩展未探索部分)
以初始化的 p[i] 为基础,向左右扩展,直到字符不匹配:
cpp
while (t[i + p[i]] == t[i - p[i]]) p[i]++;
因有哨兵符,无需判断下标越界,代码非常简洁。
这里的关键就是每个字符最多被扩展1次, R 只向右移不左移,保证总操作数是O(n)。
步骤3:更新 C 和 R (维护最右边界)
若当前 i 的回文右边界 i + p[i] > R ,说明找到更靠右的回文,同步更新:
cpp
C = i; R = i + p[i];
步骤4:记录最长回文的 max_p (最大半径)和 center_idx (对应中心)
遍历中持续对比,保留全局最大值:
cpp
if (p[i] > max_p) { max_p = p[i]; center_idx = i; }
两步还原原串的最长回文子串
- 原串起始下标: start = (center_idx - max_p) / 2 (抵消 # 的偏移,整数除法自动取整)。
- 原串回文长度: len = max_p - 1
代码(详细注释版):
cpp
class Solution {
public:
string longestPalindrome(string s) {
if (s.size() < 2) return s;
// 1. 预处理:插# + 哨兵符^$,统一奇偶
string t = "^#";
for (char c : s) { t += c; t += '#'; }
t += '$';
int m = t.size();
// 2. 初始化核心变量
vector<int> p(m, 0); // 半径数组
int C = 0, R = 0; // 最右中心C,最右边界R
int max_p = 1, center_idx = 1; // 最长半径+对应中心
// 3. 遍历预处理串(跳过哨兵符)
for (int i = 1; i < m - 1; ++i) {
// 步骤1:利用对称性初始化p[i]
if (i < R) p[i] = min(R - i, p[2*C - i]);
else p[i] = 1;
// 步骤2:暴力扩展
while (t[i + p[i]] == t[i - p[i]]) p[i]++;
// 步骤3:更新C和R
if (i + p[i] > R) { C = i; R = i + p[i]; }
// 步骤4:记录最长回文
if (p[i] > max_p) { max_p = p[i]; center_idx = i; }
}
// 4. 还原原串:起始下标+长度
int start = (center_idx - max_p) / 2;
int len = max_p - 1;
return s.substr(start, len);
}
};
这个算法的优点是时间复杂度O(n):每个字符最多遍历2次(一次复用,一次扩展),大数据量时不超时;而且无需分奇偶处理,预处理一步解决,逻辑统一。
缺点就是空间复杂度O(n):需要存储半径数组 p[] 和预处理串 t (空间换时间,工程上可接受)。
完结~