1. 回文子串问题概述
回文子串是字符串处理中的经典问题,指正读反读都相同的连续字符序列。比如"aba"、"aa"是回文,"abc"则不是。这类问题在算法面试中出现频率极高,LeetCode上相关题目超过20道,常见变体包括:
- 统计字符串中所有回文子串数量
- 寻找最长回文子串
- 判断能否通过排列组合形成回文串
C++因其高效的字符串处理能力成为解决这类问题的首选语言。标准库提供的string类与STL算法配合,能简洁地实现各类回文操作。下面通过实际代码示例,拆解三种典型解法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 中心扩展法实现
2.1 算法原理
中心扩展法基于回文的对称特性,从每个可能的中心点向两侧扩展判断。对于长度为n的字符串:
- 奇数长度回文中心是单个字符(如"aba"的中心是'b')
- 偶数长度回文中心在两个字符之间(如"abba"的中心在bb之间)
时间复杂度O(n²),空间复杂度O(1),适合中等长度字符串。
2.2 C++实现代码
cpp复制int countSubstrings(string s) {
int count = 0;
for (int i = 0; i < s.length(); ++i) {
// 奇数长度
expand(s, i, i, count);
// 偶数长度
expand(s, i, i + 1, count);
}
return count;
}
void expand(const string& s, int left, int right, int& count) {
while (left >= 0 && right < s.length() && s[left] == s[right]) {
--left;
++right;
++count;
}
}
2.3 关键点说明
- 边界处理:left不能<0,right不能≥s.length()
- 字符相等判断:s[left] == s[right]
- 计数时机:每次成功扩展后立即递增count
提示:实际面试中建议先写出expand函数框架,再补全主函数逻辑
3. 动态规划解法
3.1 DP状态定义
定义dp[i][j]表示子串s[i..j]是否为回文:
- 初始状态:单个字符dp[i][i]=true
- 状态转移:
cpp复制if (s[i] == s[j]) { dp[i][j] = (j - i <= 2) ? true : dp[i+1][j-1]; }
3.2 完整实现
cpp复制int countSubstringsDP(string s) {
int n = s.length(), res = 0;
vector<vector<bool>> dp(n, vector<bool>(n, false));
for (int i = n - 1; i >= 0; --i) {
for (int j = i; j < n; ++j) {
if (s[i] == s[j] && (j - i <= 2 || dp[i + 1][j - 1])) {
dp[i][j] = true;
++res;
}
}
}
return res;
}
3.3 性能对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 中心扩展法 | O(n²) | O(1) | 内存受限环境 |
| 动态规划 | O(n²) | O(n²) | 需要复用中间结果 |
4. Manacher算法优化
4.1 算法思想
通过插入特殊字符(如#)将字符串统一转为奇数长度,利用回文对称性避免重复计算。核心概念:
- 回文半径数组p[i]
- 当前最远右边界R
- 对称中心C
4.2 C++实现
cpp复制string preProcess(string s) {
string res = "^";
for (char c : s) res += "#" + string(1, c);
return res + "#$";
}
int manacher(string s) {
string T = preProcess(s);
int n = T.length(), C = 0, R = 0;
vector<int> P(n, 0);
for (int i = 1; i < n - 1; ++i) {
int mirror = 2 * C - i;
if (R > i) P[i] = min(R - i, P[mirror]);
while (T[i + P[i] + 1] == T[i - P[i] - 1])
++P[i];
if (i + P[i] > R) {
C = i;
R = i + P[i];
}
}
int maxLen = *max_element(P.begin(), P.end());
return maxLen;
}
4.3 复杂度分析
时间复杂度优化到O(n),适合处理超长字符串(长度>1e5)。但代码实现较复杂,面试中建议先掌握前两种方法。
5. 常见问题排查
5.1 越界访问
cpp复制// 错误示例
while (s[left] == s[right]) { // 可能越界
left--; right++;
}
// 正确写法
while (left >= 0 && right < s.length() && s[left] == s[right]) {
left--; right++;
}
5.2 初始化问题
动态规划中dp数组未正确初始化会导致错误结果:
cpp复制// 必须初始化为false
vector<vector<bool>> dp(n, vector<bool>(n, false));
5.3 性能优化技巧
- 提前终止:当剩余长度不可能超过当前最大值时可提前break
- 字符过滤:先统计各字符频率,排除不可能构成回文的情况
- 内存优化:DP解法可用滚动数组将空间降到O(n)
6. 实际应用场景
- DNA序列分析:回文结构在基因序列中具有特殊生物学意义
- 文本处理:检测回文可用于数据清洗和特征提取
- 密码学:回文性质可用于设计简单加密算法
- 竞赛题目:ACM/ICPC等编程竞赛常见题型
我在实际编码中发现,中心扩展法虽然直观,但在处理超长字符串时会遇到性能瓶颈。对于LeetCode这类在线判题系统,建议根据输入规模选择算法:
- 长度<1000:中心扩展法
- 长度1000-1e5:动态规划
- 长度>1e5:Manacher算法
