题目
给你一个字符串 s,找到 s 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1
:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2
:
输入:s = “cbbd”
输出:“bb”
方法一:中心扩展法
解题思路
- 遍历字符串s中的每个字符,以每个字符为中心向两侧扩展,找到以当前字符为中心的最长回文子串。
- 同时,考虑回文串长度为偶数的情况,以当前字符和下一个字符为中心向两侧扩展,找到以这两个字符为中心的最长回文子串。
- 在扩展过程中,记录最长回文子串的起始和结束位置。
- 最终返回最长回文子串,可以通过substring方法截取子串。
代码实现
public class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) {
return "";
}
int start = 0;
int end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i); // 以当前字符为中心,向两侧扩展
int len2 = expandAroundCenter(s, i, i + 1); // 以当前字符和下一个字符为中心,向两侧扩展
int len = Math.max(len1, len2);
if (len > end - start) {
// 更新最长回文子串的起始和结束位置
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
// 中心扩展法,返回以left和right为中心的回文子串的长度
private int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
// 返回回文子串的长度
return right - left - 1;
}
}
复杂度
时间复杂度
:O(n^2),其中n为字符串s的长度
时间复杂度
:O(1),因为只使用了常数个变量
方法二:动态规划
解题思路
- 使用动态规划来解决问题,定义一个二维布尔型数组 dp,其中
dp[i][j]
表示字符串 s 中从索引 i 到 j 的子串是否为回文串。 - 初始化 dp 数组,当 i 和 j 相等时,表示单个字符是回文串,即
dp[i][i] = true
;当s.charAt(i) == s.charAt(j)
且j - i <= 2
时,表示长度为 2 的子串是回文串,即dp[i][j] = true
。 - 遍历 dp 数组,从长度为 3 的子串开始,依次更新 dp 数组。对于 dp[i][j],如果
s.charAt(i) == s.charAt(j)
且dp[i+1][j-1]
为 true,则表示从索引 i 到 j 的子串是回文串,即dp[i][j] = true
。 - 在遍历过程中记录最长的回文子串,返回最长回文子串。
代码实现
public String longestPalindrome(String s) {
int len = s.length();
boolean[][] dp = new boolean[len][len];
String longestPalindrome = "";
int maxLength = 0;
// 初始化长度为1的回文子串
for (int i = 0; i < len; i++) {
dp[i][i] = true;
maxLength = 1;
longestPalindrome = s.substring(i, i + 1);
}
// 初始化长度为2的回文子串
for (int i = 0; i < len - 1; i++) {
if (s.charAt(i) == s.charAt(i + 1)) {
dp[i][i + 1] = true;
maxLength = 2;
longestPalindrome = s.substring(i, i + 2);
}
}
// 动态规划遍历和更新
for (int l = 3; l <= len; l++) {
for (int i = 0; i <= len - l; i++) {
int j = i + l - 1;
if (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]) {
dp[i][j] = true;
if (l > maxLength) {
maxLength = l;
longestPalindrome = s.substring(i, j + 1);
}
}
}
}
return longestPalindrome;
}
复杂度
时间复杂度
:O(n^2),其中 n 是字符串 s 的长度,因为要遍历二维数组 dp
时间复杂度
:O(n^2),需要使用二维数组 dp 来记录状态信息
方法三:Manacher算法
Manacher 算法是一种高效的解决回文子串问题的算法,其时间复杂度为 O(n)
,其中 n 是字符串的长度。以下是 Manacher 算法的解题思路、代码实现以及复杂度分析
解题思路
- Manacher 算法的核心思想是利用回文串的对称性,避免重复的比较,从而实现线性时间复杂度。
- 首先,将原始字符串进行预处理,将每个字符之间插入一个特殊的字符(如 “#”),以处理偶数长度的回文串。
- 定义一个辅助数组 P,用来记录以每个字符为中心的回文串的半径长度(包括中心字符)。
- 初始化辅助数组 P,将每个元素初始化为 1,因为每个字符本身都是一个回文串。
- 从左到右遍历字符串,利用中心扩展法找到以每个字符为中心的回文串的半径长度,并更新辅助数组 P。
- 在遍历过程中记录当前找到的最长回文串,并记录其结束位置和长度,以便最后返回结果。
代码实现
public class Solution {
public String longestPalindrome(String s) {
// 预处理字符串,在字符之间插入特殊字符 "#"
String t = preprocess(s);
// Manacher 算法核心部分
int n = t.length();
int[] dp = new int[n]; // 动态规划数组,记录每个字符作为回文中心时的回文半径长度
int center = 0; // 当前回文串的中心
int right = 0; // 当前回文串的右边界
for (int i = 0; i < n; i++) {
// 如果当前字符 i 在当前回文串的右边界内,可以利用回文对称性直接计算 dp[i]
if (i < right) {
int j = 2 * center - i; // i 关于 center 的对称点 j
dp[i] = Math.min(right - i, dp[j]);
}
// 尝试将当前字符 i 向两侧扩展,更新 dp[i]
int left = i - (dp[i] + 1); // 当前回文串的左边界
int rightBound = i + (dp[i] + 1); // 当前回文串的右边界
while (left >= 0 && rightBound < n && t.charAt(left) == t.charAt(rightBound)) {
dp[i]++;
left--;
rightBound++;
}
// 更新当前回文串的中心和右边界
if (i + dp[i] > right) {
center = i;
right = i + dp[i];
}
}
// 找出最长回文子串
int maxLen = 0; // 最长回文子串的长度
int centerIndex = 0; // 最长回文子串的中心索引
for (int i = 0; i < n; i++) {
if (dp[i] > maxLen) {
maxLen = dp[i];
centerIndex = i;
}
}
// 提取最长回文子串
StringBuilder result = new StringBuilder();
for (int i = centerIndex - maxLen; i <= centerIndex + maxLen; i++) {
if (t.charAt(i) != '#') {
result.append(t.charAt(i));
}
}
// 去除预处理时插入的特殊字符 "#"
return result.toString();
}
// 预处理字符串,在字符之间插入特殊字符 "#"
private String preprocess(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
sb.append("#");
sb.append(c);
}
sb.append("#");
return sb.toString();
}
}
复杂度
时间复杂度
: O(n),其中 n 是输入字符串的长度
空间复杂度
: O(n),因为需要使用额外的动态规划数组来保存每个字符
总结
-
中心扩展法:遍历字符串的每个字符,将其作为回文串的中心,向两侧扩展,判断扩展的字符是否相同,记录扩展的长度,最后找到最长回文子串。时间复杂度为O(n^2),空间复杂度为O(1)。
-
动态规划法:利用回文串的动态规划性质,通过动态规划数组记录每个字符作为回文中心时的回文半径长度,避免重复计算,从而找到最长回文子串。时间复杂度为O(n^2),空间复杂度为O(n)。
-
Manacher算法:利用回文串的对称性,通过动态规划的方式避免重复计算,从而在线性时间内找到最长回文子串。时间复杂度为O(n),空间复杂度为O(n)。
综上所述,中心扩展法
是简单但效率较低的方法,动态规划法
通过优化中心扩展法减少了重复计算,而Manacher算法
则是最优的方法,具有线性时间复杂度。在实际应用中,可以根据具体情况选择合适的方法来解决寻找最长回文子串的问题