1 算法题 :找到字符串中最长的回文子串
1.1 题目含义
找到字符串中最长的回文子串是一个经典的算法题。回文是指一个诗句、短语、单词、数字或其他字符序列,其从前往后和从后往前是完全相同的。因此,回文子串就是原字符串中的一个子序列,这个子序列从前往后读和从后往前读是一样的。题目要求找出给定字符串中最长的这样一个回文子串。
1.2 示例
示例 1:
输入: “babad”
输出: “bab” 或 “aba”
解释:在字符串 “babad” 中,“bab” 和 “aba” 都是回文子串,并且它们都是最长的回文子串,长度为 3。
示例 2:
输入: “cbbd”
输出: “bb”
解释:在字符串 “cbbd” 中,“bb” 是最长的回文子串,长度为2。
示例 3:
输入: “a”
输出: “a”
解释:单个字符本身就是一个回文,所以在这个例子中,整个字符串 “a” 就是最长的回文子串。
2 解题思路
解题思路如下:
对于找到字符串中最长的回文子串这个算法题,可以采用多种策略来解决。下面将分别描述中心扩展法、动态规划法和 Manacher 算法的基本解题思路。
(1)中心扩展法:
中心扩展法的基本思想是以每个字符或每对相邻字符为中心,向两边扩展,直到不能扩展为止,从而找到所有可能的回文子串。然后比较这些回文子串的长度,找出最长的那个。
具体步骤如下:
- 遍历字符串中的每个字符,以其为回文中心进行扩展,查找奇数长度的回文子串。
- 遍历字符串中每对相邻字符之间的空隙,以这两个字符的间隙为回文中心进行扩展,查找偶数长度的回文子串。
- 在每次扩展过程中,比较当前扩展得到的回文子串长度与已知的最长回文子串长度,更新最长回文子串。
(2)动态规划法:
动态规划法通过构建一个二维数组来保存子问题的解,从而避免重复计算。对于回文子串问题,可以定义一个二维数组dp,其中dp[i][j]表示从索引i到索引j的子串是否是回文串。
具体步骤如下:
- 初始化二维数组dp,将所有单个字符设为回文串(即dp[i][i] = true)。
- 从长度为2的子串开始,逐步增加子串长度,遍历所有可能的子串。
- 对于每个子串,检查其首尾字符是否相等,并且去掉首尾字符后的子串是否是回文串(即dp[i+1][j-1]是否为true)。如果满足条件,则当前子串是回文串。
- 在遍历过程中,记录并更新找到的最长回文子串及其长度。
(3)Manacher 算法:
Manacher算法是一种线性时间复杂度的算法,用于解决最长回文子串问题。它利用了回文的性质,通过预处理和巧妙的转换,将问题转化为求解最长连续相同字符序列的问题。
具体步骤如下:
- 对原始字符串进行预处理,在每个字符之间以及字符串首尾插入特殊字符(如’#'),使得所有回文子串的长度都变为奇数,从而简化问题。
- 构建一个辅助数组,用于存储每个字符关于其所在回文中心的对称位置信息。
- 从左到右遍历预处理后的字符串,同时维护当前回文中心及其对应的回文半径。
- 对于每个字符,根据辅助数组中的信息,计算其对应的回文半径,并更新最长回文子串的信息。
- 最后,根据最长回文子串的信息和预处理时添加的特殊字符,还原得到原始字符串中的最长回文子串。
3 算法实现代码
3.1 使用中心扩展法
如下为算法实现代码:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
class Solution
{
public:
// 找到最长回文子串
std::string longestPalindrome(std::string s) {
int start = 0, maxLength = 0; // 存储最长回文子串的起始位置和长度
int n = s.size();
// 遍历每个字符,以它为中心点进行扩展
for (int i = 0; i < n; i++) {
// 以当前字符为中心点的奇数长度回文串
int oddLength = expandFromCenter(s, i, i);
// 以当前字符和下一个字符的间隙为中心点的偶数长度回文串
int evenLength = expandFromCenter(s, i, i + 1);
// 取两者中较长的一个作为当前中心点的最大回文串长度
int currLength = std::max(oddLength, evenLength);
// 如果当前回文串长度大于已知的最长回文串长度,则更新最长回文串信息
if (currLength > maxLength) {
start = i - (currLength - 1) / 2; // 更新最长回文串的起始位置
maxLength = currLength; // 更新最长回文串的长度
}
}
// 返回最长回文子串
return s.substr(start, maxLength);
}
private:
// 函数用于扩展回文串,返回回文串的长度
int expandFromCenter(const std::string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
left--;
right++;
}
// 返回回文串的长度,注意这里需要加1,因为left和right在循环中分别多减和多加了一次
return right - left - 1;
}
};
上面代码定义了一个私有辅助函数 expandFromCenter,该函数以给定的左右索引为中心,尝试扩展回文串,并返回扩展后的回文串长度。然后,在 longestPalindrome 函数中,遍历字符串的每个字符,分别以该字符和该字符与下一个字符的间隙为中心,调用 expandFromCenter 函数来寻找可能的回文串。然后比较奇数长度和偶数长度的回文串,并更新最长回文串的起始位置和长度。最后,根据最长回文串的起始位置和长度,从原始字符串中截取并返回最长回文子串。
调用上面的算法,并得到输出:
int main()
{
Solution s;
std::string str = "babad";
std::string longestPalin = s.longestPalindrome(str);
std::cout << "The longest palindromic substring is: " << longestPalin << std::endl;
return 0;
}
上面代码的输出为:
The longest palindromic substring is: bab
3.2 使用动态规划法
如下为算法实现代码:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
class Solution
{
public:
// 找到最长回文子串
std::string longestPalindrome(std::string s) {
int n = s.size();
if (n < 2) return s;
// dp[i][j]表示s[i..j]是否是回文串
std::vector<std::vector<bool>> dp(n, std::vector<bool>(n, false));
int start = 0, maxLength = 1; // 记录最长回文子串的起始位置和长度
// 初始化单个字符为回文串
for (int i = 0; i < n; ++i) {
dp[i][i] = true;
}
// 从长度为2的子串开始判断是否为回文串
for (int len = 2; len <= n; ++len) {
for (int i = 0; i < n - len + 1; ++i) {
int j = i + len - 1;
if (s[i] == s[j] && (len == 2 || dp[i + 1][j - 1])) {
dp[i][j] = true;
// 更新最长回文子串的信息
if (len > maxLength) {
start = i;
maxLength = len;
}
}
}
}
// 返回最长回文子串
return s.substr(start, maxLength);
}
};
上面代码创建了一个二维布尔值数组 dp,其中 dp[i][j] 表示从索引i到索引j的子串是否是回文串。首先初始化所有单个字符为回文串,然后逐步增加子串的长度,检查所有可能的子串。
对于每个子串,检查其首尾字符是否相等,并且去掉首尾字符后的子串(如果存在)是否是回文串。如果这两个条件都满足,那么当前子串就是回文串。
在遍历过程中,还跟踪了最长回文子串的起始位置和长度,以便在算法结束时能够返回正确的结果。
最后,返回从最长回文子串的起始位置开始,长度为最长回文子串长度的子串作为结果。
4 测试用例
以下是针对上面算法的测试用例,基本覆盖了各种情况:
(1)基础测试用例:
输入: “babad”
输出: “bab” 或 “aba”
说明: 输入字符串中包含两个长度为3的回文子串 “bab” 和 “aba”。
输入: “cbbd”
输出: “bb”
说明: 输入字符串中有一个长度为2的回文子串 “bb”。
输入: “a”
输出: “a”
说明: 单个字符本身就是一个回文串。
(2)边界测试用例:
输入: “”
输出: “”
说明: 空字符串不包含任何回文子串。
输入: “aaaaaaa”
输出: “aaaaaaa”
说明: 整个字符串是一个回文串。
(3)复杂测试用例:
输入: “saturdayinightisfun”
输出: “ini”
说明: 输入字符串中包含两个长度为3的回文子串 “ini” 。
(4)特殊字符测试用例:
输入: “!”#$%&'()*+,-./:;<=>?@[\]^_{|}~"`
输出: 任意单个字符
说明: 输入字符串只包含特殊字符,每个字符单独都是回文串。