算法练习17:求最长子串(leetcode 5)

题目

给你一个字符串 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 < 2j > i, s(i, j) = s[i] === s[j]
  • 如果j - i >= 2j > 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)

你可能感兴趣的:(算法练习17:求最长子串(leetcode 5))