leetcode5 最长回文子串

259 阅读3分钟

描述:给定的字符串中找到最长的回文子串

普通算法,时间复杂度为O(n^2)


var longestPalindrome = function(s) {
  var len = s.length;
  var str = "";
  for (var i = 0; i < len; i++) {
    str = str + "#" + s[i];
  }
  str = str + "#";
  console.log("str", str);
  var p = [];
  for (var i = 0; i < str.length; i++) {
    p[i] = 0;
    var index = 1;
    for (var j = 0; j < i; j++) {
      if (str[i - index] == str[i + index]) {
        p[i] = p[i] + 1;
        index += 1;
      }
    }
  }
  var max = 0;
  var id = 0;
  p.forEach((item, index) => {
    if (item > max) {
      max = item;
      id = index;
    }
  });
  console.log("p", p);
  console.log("max,id", max, id);
  var str1 = str.slice(id - max, id + max);
  console.log("max,id", str1);
  var str2 = "";
  str1.split("").forEach(item => {
    if (item != "#") {
      str2 += item;
    }
  });
  console.log("str2", str2);
  return str2;
};

解释

字符串的预处理---添加分隔符 #

添加分割符之后就可以避免去判断是偶回文还是奇回文

偶回文 --比如 'abba'

奇回文-- 比如 'aba'

奇回文与偶回文的处理方法是不一致的,所以我们要预处理,将其变为奇回文。

我们使用一个不会出现在字符串中的字符 # 来分割字符串,这样无论是奇回文还是偶回文都会变为奇回文,便于处理。

比如: 'abba'==>'#a#b#b#a#' 以#为中心的奇回文 'aba'===> '#a#b#a#' 依旧是以b为中心的奇回文 添加的#字符起到了哨兵的作用。

辅助数组 P

数组P的每一项与预处理后的字符串是一一对应的,但是,数组P中的值表示以该位置的字符为中心的回文半径。

var s = '#a#b#a#'
 s=> 以第一个'#'为中心的回文半径是0(因为他没有回文结构)
 而以第一个a为中心的回文半径就是1(因为它两侧都有一个#,组成了回文结构)
 而已b为中心的回文半径是3。那么p数组就是【0,1,0,3,0,1,0】
 
 最长的回文半径意味着最长的回文字符串
 如上我们可以知道最长回文半径是3(redius),而且3所在的位置与最长回文的中间点的位置是一一对应的也就是是3(index).
 有了中间点的位置,有了回文半径,那么我们就可以从预处理的字符串中截取出来最长回文字符串。
 s.slice(index-redius,index+reius)

当然截取的字符串是带着 '#' 的,再做一次处理就可以得到原始的字符串了。

当然,我们也可以观察一下:
var originS = 'abc'
var s = '#a#b#a#'
(index -redius)/2 表在原始字符串中最长回文字符串的起始位置
(index+redius)/2 表示在原始字符串中最长回文字符串的结束位置

如此我们就不需要在最后去处理 字符 '#'

下标越界问题

var s = '#a#b#a#'
p=[0,1,0,3,0,1,0]
P[3]=4
最长半径(redius)为4,而回文中心点(index)为3.
我们求起点为  中心点-半径 也就是 index-redius ==>-1
这就很扯了,
所以我们将其前后再加上一些特殊的字符来保证不会出现下标越界的情况,
比如
var s = '$#a#b#a#@'

核心是我们如何去求出数组P

如下所示,我们使用双层循环来遍历每一个字符,而后求出,以该字符为中心的回文半径。

 for (var i = 0; i < str.length; i++) {
    p[i] = 0;
    var index = 1;
    for (var j = 0; j < i; j++) {
      if (str[i - index] == str[i + index]) {
        p[i] = p[i] + 1;
        index += 1;
      }
    }
  }

马拉车算法(Manacher's Algorithm)

上边我们的算法虽然解决了最长回文字符串的问题,但是,效率并不高,时间复杂度达到了O(n^2)。而马拉车算法的时间复杂度是O(n)。

双层遍历的问题
双层循环,将每一个字符都当做回文中心来进行计算回文半径,其实有些字符是完全不需要去计算回文半径的
比如例子:var s = '#a#b#a#'
我们先以第一个#为中心点求回文半径,结果是0,
而后,我们以第一个a为中心求回文半径,结果是1,
再然后是b为中心。。。,
最后我们又来到了第二个a这里,因为第二个a和第一个a是以b为中心对称的,
那么第一个a的回文半径与第二个a的回文半径是有关系的。
关系1:两者是相等的 就如上边这个例子
关系2:第二个a的回文半径要大于第一个回文半径
        比如: #a#b#a#b#a#b,
        第三个a与第一个a关于第二个a对称,
        第一个a的回文半径是1,而第三个a的回文半径则是2
        如此,我们完全以其对称的a为基点而后再往后确认回文半径长度是否会再加长。
而想要如此的前提则是,第一个和第三个a都在第二个a为中心的最长回文字符串内(也就是没有超出以第二个a为中心的回文边界mx)
但是,如果中心点超出了mx,那显然就不适用上边的特点。

根据已知的回文来减少遍历的步骤是马拉车算法的核心。

算法实现

function longestPalindrome(s) {
  var len = s.length;
  if (len < 2) {
    return s;
  }
  var str = "$";
  for (var i = 0; i < len; i++) {
    str = str + "#" + s[i];
  }
  str = str + "#@";
  var mx = 0;
  var id = 0;
  var p = [];
  var maxLength = -1;
  var index = 0;
  for (var j = 1; j < str.length - 1; j++) {
    p[j] = mx > j ? Math.min(p[2 * id - j], mx - j) : 1;
    //核心
    //2*id-j  以id为中心,那么与j对称另一端i的回文半径就是p[i]
    // i +j = 2*id ==> i = 2*id-j
    // p[i](p[2*d-j]) //只有在mx>j的情况下才会存在的
    //mx表示的是当前最长的回文的最有端所在的位置
    // 如果mx<=j,那就说明目前还没有找到一个回文结构,也就无法利用回文的特点来//减少算法了。
    
    while (str[p[j] + j] == str[j - p[j]]) {
      p[j]++;
    }
    if (mx < p[j] + j) {
      mx = p[j] + j;
      id = j;
    }
    if (maxLength < p[j] - 1) {
      maxLength = p[j] - 1;
      index = j;
    }
  }
  var start = (index - maxLength) / 2;
  var s1 = s.slice(start, start + maxLength);

  console.log("p", s1);
  return s1;
}