一、题目来源:leetcode第5题
二、题目描述
给你一个字符串
s
,找到s
中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入: s = “babad” 输出: “bab” 解释: “aba” 同样是符合题意的答案。
示例 2:
输入: s = “cbbd” 输出: “bb”
提示:
1 <= s.length <= 1000
s
仅由数字和英文字母组成
三、解题
3.1 解法一
/**
* 在给定的字符串中找到最长的回文子串。
*
* @param {string} s - 输入字符串。
* @return {string} - 找到的最长回文子串。
*/
const longestPalindrome = function(s) {
let res = '';
for (let i = 0; i < s.length; i++) {
const odd = expandAroundCenter(s, i, i);
const even = expandAroundCenter(s, i, i + 1);
const longest = odd.length > even.length ? odd : even;
if (longest.length > res.length) {
res = longest;
}
}
return res;
/**
* 在给定的字符串中找到最长的回文子串。
*
* @param {string} s - 输入字符串。
* @param {number} l - 要检查的子串的左索引。
* @param {number} r - 要检查的子串的右索引。
* @return {string} - 找到的最长回文子串。
*/
function expandAroundCenter(s, l, r) {
while (l >= 0 && r < s.length && s[l] === s[r]) {
l--;
r++;
}
return s.substring(l + 1, r);
}
};
由于遍历字符串需要 O(n) 的时间,而在每个位置上进行中心扩展需要 O(n) 的时间,因此总体时间复杂度为 O(n^2) 。
算法只使用了常数个变量来保存中间结果,没有使用额外的数据结构或数组。因此,空间复杂度可以表示为O(1) 。
3.2 解法二
马拉车算法(Manacher’s Algorithm),基本思想是利用回文串的对称性来减少不必要的重复计算。它通过维护一个回文半径数组,以及一个当前已知的最右回文边界,来快速计算每个字符位置的回文半径。该算法的具体实现比较复杂,但可以在线性时间内找到最长回文子串。
/**
* 在给定的字符串中找到最长的回文子串。
*
* @param {string} s - 输入字符串。
* @return {string} - 找到的最长回文子串。
*/
const longestPalindrome = function(s) {
// 预处理字符串,在每个字符之间插入特殊字符
const preprocessString = function(s) {
let result = '^';
for (let i = 0; i < s.length; i++) {
result += '#' + s.charAt(i);
}
result += '#$';
return result;
};
// 预处理字符串
const processedString = preprocessString(s);
const n = processedString.length;
const palindromeRadius = new Array(n).fill(0); // 回文半径数组
let center = 0; // 当前已知的最右边回文子串的中心
let right = 0; // 当前已知的最右边回文子串的右边界
for (let i = 1; i < n - 1; i++) {
// 利用对称性得到一部分回文信息
if (i < right) {
const mirror = 2 * center - i;
palindromeRadius[i] = Math.min(right - i, palindromeRadius[mirror]);
}
// 中心扩展法检查以当前字符为中心的回文子串
while (
processedString[i + 1 + palindromeRadius[i]] ===
processedString[i - 1 - palindromeRadius[i]]
) {
palindromeRadius[i]++;
}
// 更新最右边回文子串的中心和右边界
if (i + palindromeRadius[i] > right) {
center = i;
right = i + palindromeRadius[i];
}
}
// 找到最长回文子串的中心位置和长度
let maxRadius = 0;
let centerIndex = 0;
for (let i = 1; i < n - 1; i++) {
if (palindromeRadius[i] > maxRadius) {
maxRadius = palindromeRadius[i];
centerIndex = i;
}
}
// 根据中心位置和长度获取原始字符串中的最长回文子串
const start = (centerIndex - maxRadius) / 2;
const end = start + maxRadius;
return s.substring(start, end);
};
-
时间复杂度
-
预处理字符串的时间复杂度为 O(n),其中 n 是字符串的长度。在预处理过程中,我们在每个字符之间插入特殊字符(通常是#),以处理偶数长度的回文子串。
-
马拉车算法的主要部分是一个线性的循环,遍历预处理后的字符串。在该循环中,我们使用对称性和中心扩展法来计算每个字符作为回文子串中心时的回文半径。
- 对称性的计算时间复杂度为 O(n)。我们维护两个变量
center
和right
,并利用已知回文子串的对称性来更新回文半径。 - 中心扩展法的时间复杂度为 O(n)。我们在每个字符为中心时,向左右两侧扩展并检查回文性质。
- 对称性的计算时间复杂度为 O(n)。我们维护两个变量
-
因此,整个算法的时间复杂度为 O(n) 。
-
-
空间复杂度
- 预处理后的字符串需要 O(n) 的额外空间,因为我们在每个字符之间插入特殊字符。
- 回文半径数组需要 O(n) 的额外空间,用于存储每个字符作为回文子串中心时的回文半径。
- 因此,总体空间复杂度为 O(n) 。
四、知识点
双指针算法是一种常用的技巧,在解决多种问题时都可以应用。双指针的使用方式分以下3种:
- 快慢指针:快慢指针是一种特殊的双指针技巧,其中一个指针(快指针)移动的速度比另一个指针(慢指针)快。这种技巧常用于链表相关的问题,例如判断链表是否有环、找到链表的中间节点等。快慢指针可以根据问题的要求灵活调整速度和距离。
- 同向 双指针:同向双指针是指两个指针都向同一个方向移动。通常,两个指针的起始位置相同,然后逐步向右或向左移动。这种双指针移动方式常用于数组或字符串的搜索问题,例如在有序数组中查找两个数的和、找到满足特定条件的子数组等。
- 相向 双指针:相向双指针是指两个指针分别从两个不同的方向移动,通常是从数组或字符串的两端开始。这种双指针移动方式常用于有序数组或字符串的搜索问题,例如找到两个有序数组的公共元素、判断一个字符串是否为回文串等。相向双指针可以逐步缩小搜索范围,从而提高算法的效率。
双指针算法适用于解决许多问题,包括但不限于以下情况:
- 数组或字符串的搜索:当要在数组或字符串中查找特定元素或满足特定条件的子数组/子串时,可以使用双指针算法。例如,有序数组的两数之和、三数之和等问题。
- 数组或字符串的反转或交换:使用双指针可以实现对数组或字符串的反转或交换操作。例如,反转字符串、颠倒链表等问题。
- 回文问题:判断一个字符串或子串是否是回文,或者找到最长回文子串时,双指针算法是一种常用的解决方案。例如,验证回文串、最长回文子串等问题。
- 快慢指针:在链表相关问题中,使用快慢指针可以判断是否存在环、找到链表的中间节点等。快慢指针是双指针算法的一种特殊形式。
- 归并排序:归并排序中的合并操作可以使用双指针来实现。通过将两个有序数组合并为一个有序数组,双指针可以高效地完成归并排序。
- 滑动窗口:滑动窗口是一种常见的算法技巧,用于处理数组或字符串的连续子序列问题。通过使用两个指针来维护窗口的起始位置和结束位置,可以高效地解决滑动窗口相关的问题。