(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) 时间,实现较复杂)。本文的中心扩展法,是兼顾 代码简洁性空间效率 的优选方案,尤其适合面试场景快速手写。

二、算法核心思想:利用回文的"对称性"

回文的本质是"左右对称"------以某个"中心"为轴,两边的字符完全相同。因此,我们可以:

  1. 枚举所有可能的中心
    • 奇数长度回文(如"aba"):中心是单个字符(例中"b");
    • 偶数长度回文(如"aa"):中心是两个相邻字符(例中两个"a")。
  2. 向左右扩展验证:对每个中心,不断向左右两边延伸,检查两边字符是否相等,直到越界或字符不相等;
  3. 记录最长结果:在扩展过程中,实时更新"最长回文子串的长度"和"起始位置",最终通过起始位置和长度提取结果。

三、源码逐行拆解:从变量到逻辑

先贴出完整源码(与你提供的一致),再逐部分解析:

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 类型(无符号整数),但这里 iint,在字符串长度不超过 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 循环条件解析(三个条件必须同时满足):
    1. left > -1:左指针不越界(避免访问 s[-1]);
    2. right < s.size():右指针不越界(避免访问 s[s.size()],超出字符串长度);
    3. s[left] == s[right]:左右字符相等,满足回文条件;
  • 内部更新逻辑:right - left + 1 是当前回文的长度,若比 max_cnt 大,说明找到更长的回文,更新 max_cntres_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 = iright = 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 = 0max_cnt = 3,则 substr(0, 3) 返回 "bab",正是最长回文子串。

四、实战示例:代码如何运行?

以输入 s = "cbbd" 为例,一步步看代码执行过程:

  1. 初始化left=0, right=0, max_cnt=0, res_start=0
  2. 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',不相等),循环不执行;
  3. i=1(s[1]='b')
    • 场景1:left=1, right=1,回文长度1(不大于max_cnt=1),无更新;扩展后 left=0('c')vs right=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')vs right=3('d'),不相等,停止;
  4. i=2(s[2]='b')
    • 场景1:left=2, right=2,长度1 < 2,无更新;扩展后 left=1('b')vs right=3('d'),不相等;
    • 场景2:left=2, right=3('b' vs 'd',不相等),无执行;
  5. i=3(s[3]='d')
    • 场景1:长度1 < 2,无更新;
    • 场景2:right=4(超出s.size()=4),循环不执行;
  6. 返回结果substr(1, 2)"bb",正确!

五、复杂度分析:效率如何?

  • 时间复杂度 O(n²)
    遍历每个字符(O(n)),每个字符对应两次扩展(场景1和场景2),每次扩展最多遍历到字符串两端(O(n)),因此总时间为 O(n×n) = O(n²);
  • 空间复杂度 O(1)
    仅使用了 leftrightmax_cntres_start 4个变量,无额外数据结构(如数组、DP表),空间开销与字符串长度无关。

六、注意事项与优化建议

  1. 边界处理
    • 若字符串长度为1(如 s = "a"),代码会直接返回 "a"max_cnt=1res_start=0),无需额外判断;
    • 若字符串为空(s = ""),返回空串(max_cnt=0substr(0,0) 为空)。
  2. 为什么不用 at() 访问字符?
    s.at(pos) 会做越界检查,越界时抛出 out_of_range 异常;而 s[pos] 无检查,更高效。本文代码已通过 left > -1right < s.size() 手动控制边界,无需 at()
  3. 与动态规划的对比
    动态规划(DP)通过 dp[i][j] 记录 s[i..j] 是否为回文,空间复杂度 O(n²);中心扩展法空间 O(1),实现更简洁,面试中更推荐手写。

七、总结

中心扩展法解决最长回文子串问题,核心是利用回文的对称性枚举所有中心,通过"扩展-验证-更新"的流程找到最长回文。其优势在于:

  • 代码简洁,逻辑直观,10分钟内可手写完成;
  • 空间复杂度 O(1),无额外内存开销;
  • 覆盖所有回文类型(奇数/偶数长度),无遗漏。

如果你是算法新手,建议先理解"中心枚举"的思想,再逐行调试代码(比如用 s = "babad"s = "cbbd" 测试),就能轻松掌握这道经典题!

相关推荐
Qt程序员6 小时前
C++ 虚函数的使用开销以及替代方案
c++·c++设计模式·c/c++·c++虚函数
feng_blog66886 小时前
环形缓冲区实现共享内存
linux·c++
OG one.Z6 小时前
08_集成学习
人工智能·算法·机器学习
Larry_Yanan6 小时前
QML学习笔记(四十七)QML与C++交互:上下文对象
c++·笔记·qt·学习·ui
CoovallyAIHub6 小时前
超越传统3D生成:OccScene实现感知与生成的跨任务共赢
深度学习·算法·计算机视觉
Mr.H01276 小时前
克鲁斯卡尔(Kruskal)算法
数据结构·算法·图论
Tisfy6 小时前
LeetCode 3346.执行操作后元素的最高频率 I:滑动窗口(正好适合本题数据,II再另某他法)
算法·leetcode·题解·滑动窗口·哈希表
黑菜钟6 小时前
代码随想录第53天 | 图论二三题
c++·图论
CoovallyAIHub6 小时前
华为世界模型来了!30分钟生成272㎡室内场景,虚拟人导航不迷路
深度学习·算法·计算机视觉