题目
给你一个字符串 s,找到 s 中最长的回文子串
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
暴力解法
先遍历出所有的子串,再对每个子串进行回文判断
- 时间复杂度:O(n^3)
- 空间复杂度:O(1)
// 求最长回文子串
/**
* @param {string}
* @return {string}
*/
var longestPalindrome1 = function(s) {
// 边界值处理
if(!s) return ''
if(s.length == 1) return s
if(s.length == 2) {
if(s[0] === s[1]) return s
else return s[0]
}
let maxStr = ''
let max = 0
for(let i = 0; i < s.length; i++) {
for(let j = i + 1; j < s.length; j++) {
// 两指针,一指针指向当前子串最初位置,一指针指向当前子串的最末位置
let start = i
let end = j
let isPalindrome = true;
// 双指针向内移动,判断两边的字符是否是相等
while(end - start > 1) {
if(s[start] !== s[end]) {
isPalindrome = false;
break;
}
start++;
end--;
}
// 如果为奇数,那么最后一个数就不需要再判断
if(end - start == 1) isPalindrome = s[end] === s[start]
// 如果是回文,则判断当前回文子串是不是比历史记录更长,如果更长则替换
if(isPalindrome) {
if(j - i + 1 > max) {
max = j - i + 1
maxStr = s.substring(i, j+1)
}
}
}
}
// 如果所有子串都不是回文数,默认返回第一个字母
if(!maxStr) return s[0]
return maxStr;
};
let str1 = 'ac'
let res1 = longestPalindrome1(str1)
console.log(111, res1) // a
中心扩散法
遍历当前数组,把第i个值当成是回文的中心点,由中心点向两边扩散,对比是不是相等,如果相等,则继续扩散对比;否则则输出以当前字符的回文长度和回文串,每次对比历史子串大小,比较得出最大回文子串.
注意,如果是奇数子串和偶数子串对应的最大回文子串是不一样的,要区分比较
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
var longestPalindrome2 = function (s) {
// 边界处理
if(s.length < 2) return s
let maxStr = ''
let max = 0
// 因为奇偶性不一样,初始长度的左右指针是不一样的,如果是奇数,则初始左右指针指向同一个,如果是偶数,如左右指针相差1格
function getLen(s, left, right) {
while (s[left] === s[right] && left >= 0 && right <= s.length - 1) {
left--;
right++;
}
return right - left - 1;
}
let start = 0 // 指向最大子串初始位置
let end = 0 // 指向最大子串末尾位置
for (let i = 0; i < s.length; i++) {
let oddLen = getLen(s, i, i)
let evenLen = getLen(s, i, i + 1)
// 判断以当前字母,奇数个和偶数个分别对应的最长回文子串长度,取最大值
let curLen = Math.max(oddLen, evenLen)
// 如果比历史最大子串要长
if (curLen > max) {
max = curLen
// 更新左右指针位置
start = i - Math.floor((curLen - 1) / 2)
end = i + Math.floor(curLen / 2)
}
}
return s.substring(start, end + 1);
};
动态规划
1. 自顶向下递归解法
判断一个字符串是不是回文,只要最外层两数相等,里面的子串也是回文,就能证明它是一个回文串:
- 当 j - i >= 2时,s(i, j) = s[i] === s[j] && s(i +1, j - 1)
- 当 j - i < 2时,s(i, j) = s[i] === s[j]
通过以上状态方程,我们就可以写自顶向下递归写法了
- 时间复杂度:O(n^3)
- 空间复杂度:O(n*m)
var longestPalindrome = function (s) {
if(s.length < 2) return s
function isPalindrome(s, start, end) {
if(end - start < 2) return s[start] === s[end];
return s[start] === s[end] && isPalindrome(s, start + 1, end - 1)
}
let max = 0;
let maxStr = ''
// 遍历所有子串,查看是不是回文串
for(let i = 0; i < s.length; i++) {
for(let j = i + 1; j < s.length; j++) {
if(isPalindrome(s, i, j)) {
if(j - i + 1 > max) {
max = j - i + 1
maxStr = s.substring(i, j+1)
}
}
}
}
// 如果所有子串都不是回文,那么默认返回第一个字母
if(!max) return s[0]
return maxStr
};
2. 备忘录优化递归
递归优化,使用一个哈希表存储每个s(i, j)的值,避免重复计算
- 时间复杂度:O(n^3)
- 空间复杂度:O(n*m)
var longestPalindrome4 = function (s) {
if(s.length < 2) return s
let map = new Map()
function isPalindrome(s, start, end) {
let curKey = JSON.stringify([start, end])
if(map.has(curKey)) {
return map.get(curKey)
}else {
if(end - start < 2) {
map.set(curKey,s[start] === s[end])
}else {
map.set(curKey, s[start] === s[end] && isPalindrome(s, start + 1, end - 1))
}
return map.get(curKey)
}
}
let max = 0;
let maxStr = ''
for(let i = 0; i < s.length; i++) {
for(let j = i + 1; j < s.length; j++) {
if(isPalindrome(s, i, j)) {
if(j - i + 1 > max) {
max = j - i + 1
maxStr = s.substring(i, j+1)
}
}
}
}
if(!max) return s[0]
return maxStr
};
3. 动态规划
自底向上,去推导过程:
因为自身一个字母时,肯定是回文,所以s(i,i)返回true,也就是对角线上的值都为true,接着我们只需要看对角线右侧的值就可以了,因为左侧的值都是重复无效的。
那么右侧的值,我们需要按照状态转移方程去处理。
比如:‘abbd’
i/j | 0 | 1 | 2 | 3 |
---|---|---|---|---|
0 | true | - | - | - |
1 | - | true | - | - |
2 | - | - | true | - |
3 | - | - | - | true |
接下来,我们按照状态转移方程,继续填写结果就好
- 当
j - i >= 2
时,s(i, j) = s[i] === s[j] && s(i +1, j - 1)
- 当
j - i < 2
时,s(i, j) = s[i] === s[j]
推导过程:
当i=0,j =1
时,返回他们自身比较结果,即s[0] === s[1]
=> false
当i=0,j =2
时,返回他们自身比较和s(i+1, j-1)
结果,即s[0] === s[2] && s(1, 1)
=> false
当i=0,j =3
时,返回他们自身比较和s(i+1, j-1)
结果,即s[0] === s[3] && s(1, 2)
=> false
当i=1,j =2
时,返回他们自身比较结果,即s[1] === s[2]
=> true
当i=1,j =3
时,返回他们自身比较和s(i+1, j-1)
结果,即s[1] === s[3] && s(2, 2)
=> false
当i=2,j =3
时,返回他们自身比较结果,即s[2] === s[3]
=> false
i/j | 0 | 1 | 2 | 3 |
---|---|---|---|---|
0 | true | false | false | false |
1 | - | true | true | false |
2 | - | - | true | false |
3 | - | - | - | true |
从中找出规律,当j-i < 2时,返回的是两下标值的比较结果;如果>= 2,除了两下标值的比较结果,还要看其左下角的值,由此,从最小的部分开始逆推
s[i, i] = true
- 如果
j - i < 2
且j > i
,s(i, j) = s[i] === s[j]
- 如果
j - i >= 2
且j > i
,s(i, j) = s[i] === s[j] && s(i + 1, j -1)
因为当前值是可能是由其左下角的值和自身比较值决定,所以我们应该先求出最下面那行的值,由此向上递推
var longestPalindrome = function (s) {
if(s.length < 2) return s
// 当字符串长度大于2时,它的最小子串长度一定是1,我们默认其子串为第一个字母,这样就不会处理对角线上的值了
let max = 1;
let maxStr = s[0]
let curRow = new Array(s.length)
let preRow = new Array(s.length)
// 初始倒数第一行,因为字符串无论多少长度,i,j最大值是一样的,也就是在对角线上,它就是最右下角的值
preRow[s.length - 1] = true
// 从倒数第二行开始判断
for(let i = s.length - 2 ; i >= 0; i--) {
// 每一行,从倒数第一列开始遍历,直到到达对角线为止
for(let j = s.length - 1; j >= i ; j--) {
// 对角线上的值
if(i === j) {
curRow[j] = true
}else if(j - i < 2) {
curRow[j] = s[i] === s[j]
}else {
curRow[j] = (s[i] === s[j]) && preRow[j - 1]
}
// 如果当前子串是回文,跟拿它跟历史数据对比,但注意不处理对角线上的值
// 因为是倒序的,所以当前面有一样长度的值时,也要更新最长子串
if(curRow[j] && j !== i && (j - i + 1 >= max)) {
max = j - i + 1
maxStr = s.substring(i, j+1)
}
}
preRow = curRow
}
return maxStr
};
- 时间:
O(n^2)
- 空间:
O(2n) => O(n)