给你一个字符串 s,找到 s 中最长的回文子串。
什么是最回文串??
回文串
“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。
最长回文串
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
解题方法一:中心扩散法
每一个位置出发,向两边扩散查找
1、向左侧查找, 左侧元素 = 当前元素时,继续向左侧查找,否则终止
2、向右侧查找, 右侧元素 = 当前元素时,继续向右侧查找,否则终止
3、向左右查找,左侧元素 = 右侧元素时,继续查找,否则终止
上述三种情况都需要判断向左时,不能超过字符串的最左位置即:left >= 0; 向右查找时不能超过字符串的最右位置即:right < s.length
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
if(!s){
return "";
}
var slength = s.length,
maxLength = 0,
maxStart = 0;
for(var i = 0; i < slength; i++) {
// 从以i为中心,向两侧查找
var left = i-1, right = i + 1;
var curlen = 1;
// 左指针 向左移动,左侧元素与当前元素相同时curlen += 1
while(left >= 0 && s.charAt(i) === s.charAt(left)) {
left --;
curlen ++;
}
// 右指针 向右移动 右侧元素与当前元素相同时curlen += 1
while(right < slength && s.charAt(i) === s.charAt(right)) {
right ++;
curlen ++;
}
// 左指针往左,右指针往右。 当前元素的左侧和右侧相同时curlen += 2
while(left >= 0 && right < slength && s.charAt(right) === s.charAt(left)) {
left --;
right ++;
curlen += 2;
}
if(curlen > maxLength){
maxLength = curlen;
maxStart = left
}
}
// maxStart 指向的是最长回文子串的左侧
return s.substring(maxStart + 1, maxStart + maxLength + 1);
};
解题方法二:动态扩散
使用二维数组,记录子串从i到j是否为回文串,若i, j为回文串,则i-1到j-1必定为回文串
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function (s) {
// 生成二维矩阵
// 注:Array.prototype.fill() ,用一个固定值填充一个数组中从起始索引到终止索引内的全部元素,接收三个参数(value,start,end).
const initialize2DArray = (w, h, val = null) => Array(h).fill().map(() => Array(w).fill(val))
let len = s.length
if (len < 2) {
return s
}
// 记录最长子串 长度
let maxLen = 1
// 记录最长子串 开始位置
let begin = 0
// 生成一个矩阵 若从第i到第j位为回文串,则dp[i][j]赋值为true
let dp = initialize2DArray(len, len, null)
// 自己单独一个元素都是回文子串(矩阵的对角线)
for (let i = 0; i < len; i++) {
dp[i][i] = true
}
for (let j = 1; j < len; j++) {
for (let i = 0; i < j; i++) {
// s[i~j] s[i] != s[j]说明第i位到第j位这段不是回文串
if (s[i] != s[j]) {
dp[i][j] = false
} else {
// s[i] == s[j]时,如果字符串长度小于或者等于3(j-i+1 <=3),s[i~j]必定是回文串
if (j - i < 3) {
dp[i][j] = true
} else {
// s[i] == s[j]时,s[i+1,j-1]是其子串,若为回文串,则s[i,j]也是回文串;同理:子串若不是,则也不是
dp[i][j] = dp[i + 1][j - 1]
}
}
// 如果s[i,j]是回文串,且当前回文串长度大于上一次的长度(即比较每次循环的回文串长度,取最大值)
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1
begin = i
}
}
}
// 返回从begin开始致begin + maxLen的子串
return s.substring(begin, begin + maxLen)
}
解题方法三:Manacher算法
Manacher算法在中心扩散的法的基础上,存储了已经查找过的元素,然后根据回文串对称性,尽可能减少重复查找。
如下图: 回文串"ABCBDBCBA"关于D对称的两侧“C”位置(黑色箭头),两测以“C”为中心的回文串是相同的。因此当我们找到了左侧“C”为中心的回文串并且存储下来,右侧“C”为中心的回文串就不需要再查找了。这就是manacher算法核心思想,当然我们还需要对特殊情况进一步讨论。
步骤1:
为了将奇、偶数回文串统一,将原数组进行预处理,用一个字符(要求不在原字符串内)将原字符串隔开方便统一处理。
用数组p来记录每个元素为中心的回文半径(不包含自己)
如图,以下标为9的元素为中心,查找的回文串是“#D#C#A#C#D#”,则回文半径是“#D#C#” 回文半径长度为 p[9] = 5;
不难发现,处理后的字符串回文半径p[i],就是原字符串的回文串的长度:
步骤二:
找到i关于center对称的位置mirro, 以mirror为中心的回文串半径为p[mirror];
情况1:p[mirror] + i < maxRight, 则说明p[i]的回文半径不会超出以center为中心的回文串半径,直接赋值p[i] = p[mirror]即可
情况2:p[7] + i >= maxRight, 则赋值p[i] = maxRight - i,然后使用中心法则,以left = i - (1 + p[i]); right = i + (1 + p[i])尝试向两侧查找;
3、如果i + p[i] > maxRight; 此时需要挪动maxRight到当前元素为中心的回文串右边界,即maxRight = i + p[i];将center指向当前循环到的元素,即i的位置
4、判断当前p[i]是否大于maxLen,若是则更新begin和maxLen
JavaScript实现:
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function (s) {
// 给字符添加分割符
var str = addSeparator(s, '#');
var strLen = str.length;
var p = new Array(strLen);
p.fill(0);
var maxRight = 0;
var center = 0;
// 记录最长回文串的长度和起点
var maxLen = 1;
var begin = 0;
for (var i = 1; i < strLen; i++) {
if (i < maxRight) {
// i 关于center对称的位置 center - (i - center)
var morror = 2*center - i;
// 判断p[morror] + i 是否在maxRight范围内,
// 情况1: p[morror] + i < maxRight; p[i] = p[morror];
// 情况2: p[morror] + i >= maxRight; p[i] = maxRight - i; 然后中心法则继续向两侧查找
// 因此取二者的最小值即可
p[i] = Math.min(p[morror], maxRight - i);
}
// 不在范围内,中心扩散,尝试向两侧查找,针对上述情况2
var left = i - (1 + p[i]);
var right = i + (1 + p[i]);
while (left >= 0 && right < strLen && str.charAt(left) === str.charAt(right)) {
p[i] ++;
left--;
right++;
}
// 查找到当前元素的回文半径p[i],已经超过的maxRight时
if (i + p[i] > maxRight) {
// maxRight记录以当前元素为中心的回文串 最右侧位置
maxRight = i + p[i];
// center指向当前元素的位置
center = i;
}
// 比较 当前回文半径 与 maxLen,判断是否需要更新maxLen和begin
if (p[i] > maxLen) {
maxLen = p[i];
begin = (i - maxLen) / 2;
}
}
return s.substring(begin, begin + maxLen)
}
/**
* @param {string} s
* @param {string} separator
* @return {string}
*/
var addSeparator = function(s, separator){
if (s.indexOf(separator) > -1) {
throw Error('参数错误:您传入的分隔符,在传入的字符串串中存在!');
}
var str = [separator];
for(var i = 0; i < s.length; i++){
str.push(s.charAt(i))
str.push(separator)
}
return str.join('');
}