持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
给你一个字符串 s
,找到 s
中最长的回文子串。
示例 1:
输入: s = "babad"
输出: "bab"
解释: "aba" 同样是符合题意的答案。
示例 2:
输入: s = "cbbd"
输出: "bb"
提示:
1 <= s.length <= 1000
s
仅由数字和英文字母组成
本文题意很简单,想要解出来也不难,遍历所有可能的子串,判断是否是回文即可,但是该算法的时间复杂度为 O(n´³)
,时间复杂度太高,会超出时间限制,接下来就让我为您介绍两种时间复杂度为 O(n²)
的算法。
解题思路-动态规划
所谓回文字符串,就是从两端向中间或者从中间向两端,每个字符两两相等,由此我们可以知道,如果从下标 i
到 下标 j
的子串是回文串,并且 s[i-1]===s[j+1]
,则下标 i-1
到下标 j+1
的子串也是回文串。由以上推导,我们可以定义出动归的状态定义和转移方程。
状态定义:dp[i][j]
表示下标 i
到下标 j
的子串是否是回文串
转移方程:dp[i][j] = dp[i+1][j-1] && s[i]===s[j]
又因为单个字符都是回文串,所以我们可以基于这个条件初始化所有的 dp[i][i] = true
,然后不断的扩展子串长度,直到推导出所有的子串结果。
代码实现
var longestPalindrome = function(s) {
const len = s.length
if(len===1){
return s
}
const dp = Array(len)
for(let i = 0;i<len;i++){
dp[i] = []
dp[i][i] = true
}
let maxLen = 1
let beginIndex = 0
for(let L = 2;L<=len;L++){
for(let i = 0;i<=len-L;i++){
const j = i+L-1
if(s[i]!==s[j]){
dp[i][j] = false
}else{
if(i===j-1){
dp[i][j] = true
}else{
dp[i][j] = dp[i+1][j-1]
}
}
if(dp[i][j] && j-i+1>maxLen){
maxLen = j-i+1
beginIndex = i
}
}
}
return s.substring(beginIndex,beginIndex+maxLen)
}
解题思路-中心扩展
有了动归解题思路的讲解,我们知道可以如果下标 i
到下标 j
的子串是回文串,则如果 s[i-1]===s[j+1]
,下标 i-1
到下标 j+1
的子串也是回文串。由此可以想到可以枚举所有可能的回文串中心,通过向两侧扩展的方式,找到所有的回文子串,从而得到最长的回文子串。
代码实现
function getMaxBackTextString(s,len,l,r){
if(s[l]!==s[r]){
return 0
}
let res = r-l+1
while(l>0 && r<len-1){
l--
r++
if(s[l]===s[r]){
res+=2
}else{
break
}
}
return res
}
var longestPalindrome = function(s) {
const len = s.length
if(len===1){
return s
}
let maxLen = 1
let beginIndex = 0
for(let i = 0;i<len;i++){
const len1 = getMaxBackTextString(s,len,i,i)
const len2 = getMaxBackTextString(s,len,i,i+1)
if(len1>maxLen){
maxLen = len1
beginIndex = i-(len1-1)/2
}
if(len2>maxLen){
maxLen = len2
beginIndex = i-(len2-2)/2
}
}
return s.substring(beginIndex,beginIndex+maxLen)
}
需要注意的是,虽然都是O(n²)
的时间复杂度,但是中心扩展要比动态规划更高效,这是因为动态规划初始化的过程是 O(n)
,而且它的推导过程是完整的。而中心扩展法遇到无法扩展就停止了,而且每次扩展是两个字符长度。
还有一部分原因是动态规划的空间复杂度也是O(n²)
,而中心扩展的空间复杂度仅为O(1)
.
至此我们就完成了 leetcode-5-最长回文子串
如有任何问题或建议,欢迎留言讨论!