最长回文子串
1.题目描述
给你一个字符串 s
,找到 s
中最长的 回文 子串。
示例 1:
arduino
输入: s = "babad"
输出: "bab"
解释: "aba" 同样是符合题意的答案。
示例 2:
ini
输入: s = "cbbd"
输出: "bb"
提示:
1 <= s.length <= 1000
s
仅由数字和英文字母组成
2.解决方案
1.暴力解法
- 思路:
- 枚举字符串
s
的所有子串,对于每个子串,检查它是否是回文串。 - 从长度为 1 的子串开始,逐渐增加子串长度,记录下最长的回文子串。
- 代码实现:
ts
function longestPalindromeBruteForce(s: string): string {
let maxLength = 0;
let result = '';
const n = s.length;
for (let i = 0; i < n; i++) {
for (let j = i; j < n; j++) {
let isPalindrome = true;
for (let k = 0; k < (j - i + 1) / 2; k++) {
if (s[i + k]!== s[j - k]) {
isPalindrome = false;
break;
}
}
if (isPalindrome && (j - i + 1) > maxLength) {
maxLength = j - i + 1;
result = s.slice(i, j + 1);
}
}
}
return result;
}
- 分析:
- 时间复杂度 :(O(n^3))。外层循环遍历子串的起始位置
i
,时间复杂度为 (O(n));中层循环遍历子串的结束位置j
,时间复杂度为 (O(n));内层循环检查子串是否为回文,时间复杂度为 (O(n))。所以总的时间复杂度为 (O(n \times n \times n))。 - 空间复杂度:(O(1)),除了存储结果的字符串外,只使用了常数级别的额外空间。
- 缺点:时间复杂度极高,对于较长的字符串,运行效率极低,会导致超时。
2.中心扩展算法
- 思路:
- 回文串的特点是关于中心对称,所以可以以每个字符和相邻字符间隙为中心,向两边扩展,检查扩展出的子串是否为回文。
- 对于长度为
n
的字符串,有2n - 1
个可能的中心(n
个字符作为单字符中心,n - 1
个相邻字符间隙作为双字符中心)。
- 代码实现:
ts
function longestPalindrome(s: string): string {
let start = 0;
let maxLength = 0;
const n = s.length;
for (let i = 0; i < n; i++) {
// 以单个字符为中心扩展
let left1 = i;
let right1 = i;
while (left1 >= 0 && right1 < n && s[left1] === s[right1]) {
if (right1 - left1 + 1 > maxLength) {
maxLength = right1 - left1 + 1;
start = left1;
}
left1--;
right1++;
}
// 以两个相邻字符为中心扩展
let left2 = i;
let right2 = i + 1;
while (left2 >= 0 && right2 < n && s[left2] === s[right2]) {
if (right2 - left2 + 1 > maxLength) {
maxLength = right2 - left2 + 1;
start = left2;
}
left2--;
right2++;
}
}
return s.slice(start, start + maxLength);
}
- 分析:
- 时间复杂度 :(O(n^2))。对于每个可能的中心,最多需要扩展
n
次,而总共有2n - 1
个中心,所以时间复杂度为 (O(n \times n))。 - 空间复杂度:(O(1)),只使用了常数级别的额外空间。
- 优点:相比暴力解法,时间复杂度有所降低,在实际应用中效率更高。
3.Manacher 算法
- 思路:
- Manacher 算法通过对字符串进行预处理,将奇数长度和偶数长度的回文串统一处理。
- 它利用已经计算出的回文子串信息,避免了重复计算,从而将时间复杂度优化到 (O(n))。
- 具体来说,算法使用一个数组
p
记录以每个字符为中心的回文半径,通过巧妙的计算和更新,快速找到最长回文子串。
- 代码实现:
ts
function longestPalindromeManacher(s: string): string {
// 预处理字符串
const newS = '#';
for (const char of s) {
newS += char + '#';
}
const n = newS.length;
const p: number[] = new Array(n).fill(0);
let center = 0;
let right = 0;
for (let i = 0; i < n; i++) {
let iMirror = 2 * center - i;
if (right > i) {
p[i] = Math.min(right - i, p[iMirror]);
} else {
p[i] = 0;
}
// 尝试扩展
while (i + (1 + p[i]) < n && i - (1 + p[i]) >= 0 && newS[i + (1 + p[i])] === newS[i - (1 + p[i])]) {
p[i]++;
}
// 更新中心和右边界
if (i + p[i] > right) {
center = i;
right = i + p[i];
}
}
let maxLen = 0;
let maxCenter = 0;
for (let i = 0; i < n; i++) {
if (p[i] > maxLen) {
maxLen = p[i];
maxCenter = i;
}
}
const start = (maxCenter - maxLen) / 2;
return s.slice(start, start + maxLen);
}
- 分析:
- 时间复杂度:(O(n))。虽然代码中有多层循环,但由于巧妙地利用了已有的回文信息,每个字符最多被访问常数次,所以时间复杂度为 (O(n))。
- 空间复杂度 :(O(n)),需要一个数组
p
来记录回文半径。
- 优点:时间复杂度最优,在处理非常长的字符串时,性能远远优于暴力解法和中心扩展算法。
4.最优解及原因
- 最优解:Manacher 算法是最优解。
- 原因:当字符串长度较大时,时间复杂度是衡量算法优劣的关键指标。Manacher 算法将时间复杂度优化到了线性的 (O(n)),相比暴力解法的 (O(n^3)) 和中心扩展算法的 (O(n^2)),在处理大规模数据时效率有显著提升。虽然它需要 (O(n)) 的额外空间,但对于追求高效的场景,这种以空间换时间的方式是值得的。
3.拓展和题目变形
拓展:
- 找到所有最长回文子串。
思路:
- 在 Manacher 算法的基础上,记录所有达到最大回文半径的中心位置,然后根据这些位置还原出所有最长回文子串。
代码实现:
ts
function findAllLongestPalindromes(s: string): string[] {
const newS = '#';
for (const char of s) {
newS += char + '#';
}
const n = newS.length;
const p: number[] = new Array(n).fill(0);
let center = 0;
let right = 0;
for (let i = 0; i < n; i++) {
let iMirror = 2 * center - i;
if (right > i) {
p[i] = Math.min(right - i, p[iMirror]);
} else {
p[i] = 0;
}
while (i + (1 + p[i]) < n && i - (1 + p[i]) >= 0 && newS[i + (1 + p[i])] === newS[i - (1 + p[i])]) {
p[i]++;
}
if (i + p[i] > right) {
center = i;
right = i + p[i];
}
}
let maxLen = 0;
const maxCenters: number[] = [];
for (let i = 0; i < n; i++) {
if (p[i] > maxLen) {
maxLen = p[i];
maxCenters.length = 0;
maxCenters.push(i);
} else if (p[i] === maxLen) {
maxCenters.push(i);
}
}
const results: string[] = [];
for (const maxCenter of maxCenters) {
const start = (maxCenter - maxLen) / 2;
results.push(s.slice(start, start + maxLen));
}
return results;
}
题目变形:
- 给定一个字符串
s
和一个整数k
,找到长度至少为k
的最长回文子串。
思路:
- 可以在中心扩展算法或 Manacher 算法的基础上进行修改。在扩展或计算回文半径时,当找到的回文子串长度达到或超过
k
时,记录下来并继续寻找更长的满足条件的回文子串。
代码实现(基于中心扩展算法) :
ts
function longestPalindromeWithMinLength(s: string, k: number): string {
let start = 0;
let maxLength = 0;
const n = s.length;
for (let i = 0; i < n; i++) {
let left1 = i;
let right1 = i;
while (left1 >= 0 && right1 < n && s[left1] === s[right1]) {
if (right1 - left1 + 1 >= k && right1 - left1 + 1 > maxLength) {
maxLength = right1 - left1 + 1;
start = left1;
}
left1--;
right1++;
}
let left2 = i;
let right2 = i + 1;
while (left2 >= 0 && right2 < n && s[left2] === s[right2]) {
if (right2 - left2 + 1 >= k && right2 - left2 + 1 > maxLength) {
maxLength = right2 - left2 + 1;
start = left2;
}
left2--;
right2++;
}
}
return maxLength >= k? s.slice(start, start + maxLength) : '';
}