(huawei)5.最长回文子串
详解最长回文子串:中心扩展法源码拆解与实战分析(LeetCode 5)
在算法学习中,「最长回文子串」是一道绕不开的经典题目(LeetCode 第 5 题)。它不仅考察对回文特性的理解,更考验如何在效率与实现复杂度之间找到平衡。本文将基于 中心扩展法 的完整源码,从算法思想、代码拆解、实战验证到复杂度分析,一步步带你掌握这道题的最优解之一。
一、问题回顾:什么是“最长回文子串”?
首先明确问题边界:给定一个字符串 s,找到其中最长的回文子串(回文是指正读和反读都一样的字符串,如“aba”“aa”)。例如:
- 输入
s = "babad",输出可以是"bab"或"aba"; - 输入
s = "cbbd",输出是"bb"。
常见解法有暴力法(O(n³) 时间,低效)、动态规划(O(n²) 时间+O(n²) 空间)、中心扩展法(O(n²) 时间+O(1) 空间)和 Manacher 算法(O(n) 时间,实现较复杂)。本文的中心扩展法,是兼顾 代码简洁性 和 空间效率 的优选方案,尤其适合面试场景快速手写。
二、算法核心思想:利用回文的“对称性”
回文的本质是“左右对称”——以某个“中心”为轴,两边的字符完全相同。因此,我们可以:
- 枚举所有可能的中心:
- 奇数长度回文(如“aba”):中心是单个字符(例中“b”);
- 偶数长度回文(如“aa”):中心是两个相邻字符(例中两个“a”)。
- 向左右扩展验证:对每个中心,不断向左右两边延伸,检查两边字符是否相等,直到越界或字符不相等;
- 记录最长结果:在扩展过程中,实时更新“最长回文子串的长度”和“起始位置”,最终通过起始位置和长度提取结果。
三、源码逐行拆解:从变量到逻辑
先贴出完整源码(与你提供的一致),再逐部分解析:
class Solution {
public:string longestPalindrome(string s) {// 1. 初始化变量int left = 0, right = 0, max_cnt = 0, res_start = 0;// 2. 遍历每个字符,作为回文中心for (int i = 0; i < s.size(); i++) {// 3. 情况1:以单个字符 i 为中心(对应奇数长度回文,如 "aba")left = i;right = i;while (left > -1 && right < s.size() && s[left] == s[right]) {// 更新最长回文的长度和起始位置if (right - left + 1 > max_cnt) {max_cnt = right - left + 1;res_start = left;}// 向左右扩展left--;right++;}// 4. 情况2:以 i 和 i+1 为中心(对应偶数长度回文,如 "aa")left = i;right = i + 1;while (left > -1 && right < s.size() && s[left] == s[right]) {// 更新最长回文的长度和起始位置if (right - left + 1 > max_cnt) {max_cnt = right - left + 1;res_start = left;}// 向左右扩展left--;right++;}}// 5. 提取并返回最长回文子串return s.substr(res_start, max_cnt);}
};
3.1 变量初始化:4个核心变量的作用
int left = 0, right = 0, max_cnt = 0, res_start = 0;
left/right:扩展时的左右指针,分别指向当前回文的左边界和右边界;max_cnt:记录最长回文子串的长度(初始为0,后续动态更新);res_start:记录最长回文子串的起始索引(初始为0,配合max_cnt最终提取结果)。
3.2 遍历中心:for循环的意义
for (int i = 0; i < s.size(); i++) { ... }
- 遍历字符串的每个字符
s[i],将其作为“潜在中心”的起点; - 为什么用
s.size()?因为要遍历到最后一个字符(i最大为s.size()-1),避免遗漏任何可能的中心; - 注意:
s.size()返回size_t类型(无符号整数),但这里i是int,在字符串长度不超过int最大值时(日常场景均满足),无需担心类型问题。
3.3 两种扩展场景:覆盖所有回文类型
场景1:单个字符为中心(奇数长度回文)
left = i;
right = i;
while (left > -1 && right < s.size() && s[left] == s[right]) {// 更新最长回文信息if (right - left + 1 > max_cnt) {max_cnt = right - left + 1;res_start = left;}// 扩展left--;right++;
}
- 初始时
left = right = i:以s[i]为中心,此时回文长度为1(单个字符本身就是回文); while循环条件解析(三个条件必须同时满足):left > -1:左指针不越界(避免访问s[-1]);right < s.size():右指针不越界(避免访问s[s.size()],超出字符串长度);s[left] == s[right]:左右字符相等,满足回文条件;
- 内部更新逻辑:
right - left + 1是当前回文的长度,若比max_cnt大,说明找到更长的回文,更新max_cnt和res_start(起始位置是left,因为left是当前回文的左边界); - 扩展操作:
left--(左移)、right++(右移),继续验证下一对字符。
场景2:两个相邻字符为中心(偶数长度回文)
left = i;
right = i + 1;
while (left > -1 && right < s.size() && s[left] == s[right]) {// 同场景1:更新最长回文信息if (right - left + 1 > max_cnt) {max_cnt = right - left + 1;res_start = left;}// 扩展left--;right++;
}
- 初始时
left = i、right = i+1:以s[i]和s[i+1]为中心,此时回文长度为2(需满足s[i] == s[i+1]才是回文); - 其余逻辑与场景1完全一致,目的是覆盖偶数长度的回文(如“aa”“abba”)。
3.4 结果提取:substr的妙用
return s.substr(res_start, max_cnt);
std::string::substr(pos, len)函数:从索引pos开始,提取长度为len的子串;- 例如:若
s = "babad",最终res_start = 0、max_cnt = 3,则substr(0, 3)返回"bab",正是最长回文子串。
四、实战示例:代码如何运行?
以输入 s = "cbbd" 为例,一步步看代码执行过程:
- 初始化:
left=0, right=0, max_cnt=0, res_start=0; - i=0(s[0]=‘c’):
- 场景1:
left=0, right=0,回文长度1 > 0 →max_cnt=1, res_start=0;扩展后left=-1,循环停止; - 场景2:
left=0, right=1(s[0]=‘c’ vs s[1]=‘b’,不相等),循环不执行;
- 场景1:
- i=1(s[1]=‘b’):
- 场景1:
left=1, right=1,回文长度1(不大于max_cnt=1),无更新;扩展后left=0(‘c’)vsright=2(‘b’),不相等,停止; - 场景2:
left=1, right=2(s[1]=‘b’ vs s[2]=‘b’,相等),回文长度2 > 1 →max_cnt=2, res_start=1;扩展后left=0(‘c’)vsright=3(‘d’),不相等,停止;
- 场景1:
- i=2(s[2]=‘b’):
- 场景1:
left=2, right=2,长度1 < 2,无更新;扩展后left=1(‘b’)vsright=3(‘d’),不相等; - 场景2:
left=2, right=3(‘b’ vs ‘d’,不相等),无执行;
- 场景1:
- i=3(s[3]=‘d’):
- 场景1:长度1 < 2,无更新;
- 场景2:
right=4(超出s.size()=4),循环不执行;
- 返回结果:
substr(1, 2)→"bb",正确!
五、复杂度分析:效率如何?
- 时间复杂度 O(n²):
遍历每个字符(O(n)),每个字符对应两次扩展(场景1和场景2),每次扩展最多遍历到字符串两端(O(n)),因此总时间为 O(n×n) = O(n²); - 空间复杂度 O(1):
仅使用了left、right、max_cnt、res_start4个变量,无额外数据结构(如数组、DP表),空间开销与字符串长度无关。
六、注意事项与优化建议
- 边界处理:
- 若字符串长度为1(如
s = "a"),代码会直接返回"a"(max_cnt=1,res_start=0),无需额外判断; - 若字符串为空(
s = ""),返回空串(max_cnt=0,substr(0,0)为空)。
- 若字符串长度为1(如
- 为什么不用
at()访问字符?
s.at(pos)会做越界检查,越界时抛出out_of_range异常;而s[pos]无检查,更高效。本文代码已通过left > -1和right < s.size()手动控制边界,无需at()。 - 与动态规划的对比:
动态规划(DP)通过dp[i][j]记录s[i..j]是否为回文,空间复杂度 O(n²);中心扩展法空间 O(1),实现更简洁,面试中更推荐手写。
七、总结
中心扩展法解决最长回文子串问题,核心是利用回文的对称性枚举所有中心,通过“扩展-验证-更新”的流程找到最长回文。其优势在于:
- 代码简洁,逻辑直观,10分钟内可手写完成;
- 空间复杂度 O(1),无额外内存开销;
- 覆盖所有回文类型(奇数/偶数长度),无遗漏。
如果你是算法新手,建议先理解“中心枚举”的思想,再逐行调试代码(比如用 s = "babad" 或 s = "cbbd" 测试),就能轻松掌握这道经典题!
