详解最长回文子串:中心扩展法源码拆解与实战分析(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")。
 
- 向左右扩展验证:对每个中心,不断向左右两边延伸,检查两边字符是否相等,直到越界或字符不相等;
- 记录最长结果:在扩展过程中,实时更新"最长回文子串的长度"和"起始位置",最终通过起始位置和长度提取结果。
三、源码逐行拆解:从变量到逻辑
先贴出完整源码(与你提供的一致),再逐部分解析:
            
            
              cpp
              
              
            
          
          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个核心变量的作用
            
            
              cpp
              
              
            
          
          int left = 0, right = 0, max_cnt = 0, res_start = 0;- left/- right:扩展时的左右指针,分别指向当前回文的左边界和右边界;
- max_cnt:记录最长回文子串的长度(初始为0,后续动态更新);
- res_start:记录最长回文子串的起始索引 (初始为0,配合- max_cnt最终提取结果)。
3.2 遍历中心:for循环的意义
            
            
              cpp
              
              
            
          
          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:单个字符为中心(奇数长度回文)
            
            
              cpp
              
              
            
          
          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:两个相邻字符为中心(偶数长度回文)
            
            
              cpp
              
              
            
          
          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的妙用
            
            
              cpp
              
              
            
          
          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" 测试),就能轻松掌握这道经典题!