算法——【最长回文子串】

前言

学习算法之前,我们先理清楚几个基础概念:

回文串:正读和反读一样的串

回文子串:原文的某个回文串

最长回文子串:原串中最长的回文子串

接下来,我们看看问题是什么:

给你一个字符串 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³),在长度较长时,会超时,不适合实际工程和面试场景。

中心扩展法

先说说大致思路:利用回文的中心对称特性,枚举所有可能的回文中心,然后向两边扩展。

回文中心分两种:

  1. 奇数长度回文:中心是单个字符,对应 expandAroundCenter(s, i, i)
  2. 偶数长度回文:中心是两个字符的间隙,对应 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种情况,只初始化,不扩展:

  1. i < R(已探索区): p[i] = min(R - i, p[2C - i]) ( 2C - i 是对称点, R-i 是i在已探索区的最大可复用半径,取小值避免越界)。
  2. 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; }
两步还原原串的最长回文子串
  1. 原串起始下标: start = (center_idx - max_p) / 2 (抵消 # 的偏移,整数除法自动取整)。
  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 (空间换时间,工程上可接受)。

完结~

相关推荐
议题一玩到2 小时前
#leetcode# 1984. Minimum Difference Between Highest and Lowest of K Scores
数据结构·算法·leetcode
你撅嘴真丑2 小时前
计算2的N次方 和 大整数的因子
数据结构·c++·算法
孞㐑¥2 小时前
算法—前缀和
c++·经验分享·笔记·算法
CSDN_RTKLIB2 小时前
【编码实战】编译器解码编码过程
c++
yugi9878382 小时前
基于MATLAB的延迟求和(DAS)波束形成算法实现
开发语言·算法·matlab
漫随流水2 小时前
leetcode回溯算法(90.子集Ⅱ)
数据结构·算法·leetcode·回溯算法
Yupureki2 小时前
《算法竞赛从入门到国奖》算法基础:搜索-记忆化搜索
c语言·c++·学习·算法·深度优先
June bug2 小时前
(#数组/链表操作)合并两个有重复元素的无序数组,返回无重复的有序结果
数据结构·python·算法·leetcode·面试·跳槽
普贤莲花2 小时前
取舍~2026年第4周小结---写于20260125
程序人生·算法·leetcode