思路:枚举所有可能的子串,然后判断每个子串是否是回文串,最后找出最长的回文子串。
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
if (n == 0) {
return "";
}
String maxPalindrome = s.substring(0, 1); // 初始化为第一个字符
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (isPalindrome(s, i, j)) {
if (j - i + 1 > maxPalindrome.length()) {
maxPalindrome = s.substring(i, j + 1);
}
}
}
}
return maxPalindrome;
}
private boolean isPalindrome(String s, int left, int right) {
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
}
暴力法的时间复杂度是 O(n^3)
,其中 n
是字符串的长度。这是因为枚举所有子串需要 O(n^2)
的时间,而判断每个子串是否是回文串需要 O(n)
的时间。
这种方法在字符串较长时效率较低,不适用于大规模数据。
中心扩展法:回文子串的中心可能是一个字符(奇数长度)或两个相同字符(偶数长度)。对于字符串中的每个字符,都检查以它为中心的回文子串长度。
初始化变量:start
和 end
用于记录最长回文子串的起始和结束位置。
循环遍历:对每个字符进行中心扩展,分别处理奇数和偶数长度的回文子串。
更新最大回文子串:每次扩展后,比较当前回文子串的长度,更新 start
和 end
。
返回结果:根据 start
和 end
提取最长回文子串。
class Solution {
public String longestPalindrome(String s) {
// 如果输入的字符串为空或者长度小于1,直接返回空字符串(因为无法构成回文)
if (s == null || s.length() < 1) {
return "";
}
// 初始化最长回文子串的起始和结束位置(初始时假设第一个字符就是最长回文子串)
int start = 0, end = 0;
// 遍历字符串中的每个字符,以每个字符为中心进行扩展查找回文子串
for (int i = 0; i < s.length(); i++) {
// 寻找以 i 为中心的奇数长度回文子串的长度
int len1 = expandAroundCenter(s, i, i);
// 寻找以 i 和 i+1 为中心的偶数长度回文子串的长度
int len2 = expandAroundCenter(s, i, i + 1);
// 取两种情况下的最大回文子串长度
int len = Math.max(len1, len2);
// 如果当前找到的回文子串长度大于之前记录的最大长度,则更新起始和结束位置
if (len > end - start) {
// 根据回文子串的长度 len 和中心位置 i,计算新的起始位置
start = i - (len - 1) / 2;
// 计算新的结束位置
end = i + len / 2;
}
}
// 根据起始和结束位置,截取最长回文子串并返回
return s.substring(start, end + 1);
}
// 定义一个辅助方法,用于从中心向两边扩展查找回文子串
private int expandAroundCenter(String s, int left, int right) {
// 当左右指针未越界且左右字符相等时,继续向外扩展
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--; // 左指针左移
right++; // 右指针右移
}
// 返回回文子串的长度(结束位置减去开始位置再减1)
return right - left - 1;
}
}
时间复杂度: O(n^2)
,其中 n
是字符串的长度,空间复杂度是 O(1)
。
它适用于题目中给出的约束条件(字符串长度最多为1000)。
状态定义:
使用一个二维数组 dp[i][j]
,表示子串 s[i..j]
是否是回文串。
dp[i][j] = true
表示子串 s[i..j]
是回文串,false
表示不是。
状态转移方程:
如果 s[i] != s[j]
,则 dp[i][j] = false
(子串两端字符不相等,不是回文串)。
如果 s[i] == s[j]
:
当子串长度为 2 时(即 j = i + 1
),dp[i][j] = true
。
当子串长度大于 2 时,dp[i][j] = dp[i + 1][j - 1]
(取决于内部子串是否是回文串)。
初始化:
所有长度为 1 的子串都是回文串,即 dp[i][i] = true
。
遍历顺序:
按子串长度从 2 到 n 逐步增加,枚举所有可能的子串起始位置 i
,计算对应的结束位置 j
。
记录最长回文子串:
在遍历过程中,记录最长回文子串的长度 maxLen
和起始位置 begin
,最后通过 substring
方法提取结果。
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
if (n == 0) {
return "";
}
// 初始化一个二维数组 dp,dp[i][j] 表示子串 s[i..j] 是否是回文串
boolean[][] dp = new boolean[n][n];
int maxLen = 1; // 最长回文子串的长度,初始为1(单个字符)
int begin = 0; // 最长回文子串的起始位置
// 所有长度为1的子串都是回文串
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 枚举子串长度,从2到n
for (int L = 2; L <= n; L++) {
// 枚举子串的起始位置 i
for (int i = 0; i < n; i++) {
// 子串的结束位置 j = i + L - 1
int j = i + L - 1;
if (j >= n) {
break; // 如果结束位置超出字符串范围,跳出循环
}
// 如果子串两端字符不相等,则不是回文串
if (s.charAt(i) != s.charAt(j)) {
dp[i][j] = false;
} else {
// 如果子串两端字符相等,则需要判断内部子串是否是回文串
// 当子串长度为2时,内部没有字符,直接是回文串
if (L == 2) {
dp[i][j] = true;
} else {
// 内部子串是否是回文串取决于 dp[i+1][j-1]
dp[i][j] = dp[i + 1][j - 1];
}
}
// 如果当前子串是回文串且长度大于 maxLen,更新 maxLen 和 begin
if (dp[i][j] && L > maxLen) {
maxLen = L;
begin = i;
}
}
}
// 返回最长回文子串
return s.substring(begin, begin + maxLen);
}
}
时间复杂度: O(n^2)
,
空间复杂度是 O(n^2)
,
其中 n
是字符串的长度。它适用于题目中给出的约束条件(字符串长度最多为1000)。
核心思想:Manacher 算法通过巧妙地将所有可能的回文子串统一为奇数长度的形式,利用对称性和已知回文信息来避免重复计算,从而实现高效的最长回文子串查找。
class Solution {
public String longestPalindrome(String s) {
// 如果输入字符串为空或者长度为0,直接返回空字符串
if (s == null || s.length() == 0) {
return "";
}
// 在字符串中插入特殊字符 '#',将所有可能的回文子串统一为奇数长度的形式
String transformed = "#" + String.join("#", s.split("")) + "#";
int n = transformed.length(); // 获取预处理后字符串的长度
int[] p = new int[n]; // p[i] 表示以 i 为中心的回文半径
int center = 0, right = 0; // center 是当前回文右边界的中心,right 是当前回文右边界
int maxLen = 0, maxCenter = 0; // 记录最长回文半径和其中心位置
// 遍历预处理后的字符串
for (int i = 0; i < n; i++) {
// 计算当前位置 i 的镜像位置 mirror
int mirror = 2 * center - i;
// 如果当前位置 i 在当前回文右边界内,则利用镜像位置 mirror 的信息初始化 p[i]
if (i < right) {
p[i] = Math.min(right - i, p[mirror]);
}
// 尝试扩展回文边界,直到无法扩展为止
while (i - p[i] - 1 >= 0 && i + p[i] + 1 < n && transformed.charAt(i - p[i] - 1) == transformed.charAt(i + p[i] + 1)) {
p[i]++; // 扩展成功,回文半径增加
}
// 如果当前位置 i 的回文右边界超过了之前的 right,更新 center 和 right
if (i + p[i] > right) {
center = i;
right = i + p[i];
}
// 更新最长回文半径及其对应的中心位置
if (p[i] > maxLen) {
maxLen = p[i];
maxCenter = i;
}
}
// 计算最长回文子串在原始字符串中的起始位置和长度
int start = (maxCenter - maxLen) / 2;
int length = maxLen;
// 提取并返回最长回文子串
return s.substring(start, start + length);
}
}
字符串预处理:
在原始字符串中插入特殊字符 #
,使得所有可能的回文子串都转换为奇数长度的形式。例如,字符串 "abc"
转换为 "#a#b#c#"
。
变量初始化:
p[i]
表示以位置 i
为中心的回文半径。
center
和 right
分别表示当前回文右边界的中心位置和右边界位置。
maxLen
和 maxCenter
用于记录最长回文半径及其对应的中心位置。
主循环:
遍历预处理后的字符串,计算每个位置 i
的回文半径 p[i]
。
利用镜像位置 mirror
的信息初始化 p[i]
,以避免重复计算。
尝试扩展回文边界,更新 p[i]
的值。
如果当前回文右边界超过之前的 right
,更新 center
和 right
。
结果提取:
根据最长回文半径 maxLen
和中心位置 maxCenter
,计算原始字符串中的起始位置和子串长度。
使用 substring
方法提取并返回最长回文子串。
Manacher 算法通过巧妙的预处理和镜像对称的性质,将最长回文子串问题的时间复杂度降低到 O(n)
,非常高效。虽然实现上稍显复杂,但它是最长回文子串问题的最优解之一。