目录
一、题目描述
给你一个字符串s,找到s中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"提示:
1 <= s.length <= 1000
s 仅由数字和英文字母组成
二、解题思路
1、动态规划
如果s字符串的一个子串是回文串且长度大于2,那么在移除其首尾两个字母后,它仍然还会是回文串。例如字符串“abcba”,假设我们已知“bcb”是一个回文串,那么可以确定“abcba”也一定是回文串,因为其首尾字母均为“a”。所以,本题我们可以使用动态规划的算法思想来解决。
1)确定dp数组及其含义
定义一个二维dp数组
boolean[][] dp= new boolean[length][length];
注:能用基本类型的尽量用基本类型,基本类型的使用效率要比对应的包装类要好,而且占用的内存更少。
length
表示字符串 s
的长度,而 dp[i][j]
表示字符串 s
中从位置 i
到 j
的子串是否为回文串。如果该子串是回文,则 dp[i][j]
的值为 true
,否则为 false
。根据 dp[i][j]
的定义是判断位置 i
到 j
的子串是否为回文,因此 i
必然小于等于 j
。这也就是说,当我们在构建这个二维动态规划数组时,我们只需推导对角线上半部分的值即可。
2)确定状态转移方程式
因为当“aba”是回文子串的时候,那么“cabac”是不是回文子串,是取决于新加入的两个字符是否相等的。所以,不难得出状态转移方程式:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
根据状态转移方程式,不难看出,该方程式只是适用于回文子串长度大于等于2的情况。回文子串为1的时候是不适合该表达式的。其实回文子串为1的时候,我们是不需要考虑的。因为,如果字符串s的最大回文子串就是单字符的时候,我们只接就是按照默认值进行截取就行了。
3)对dp数组进行初始化
在动态规划中,通常需要初始化状态数组以存储中间结果。然而对于本题,我们并不需要预先填充动态规划数组。这是因为,当考虑的回文子串长度小于等于3时,我们只需比较i和j位置的字符是否相等,便可直接判定它们是否是回文子串。在此基础上,更长回文子串的状态可以通过已确定的状态推导得出,无需依赖预先设定的初始值。因此,在这种特殊情况下,初始化动态规划数组实际上是不必要的,我们可以直接根据字符比较的结果来逐步构建整个数组。
4)确定dp数组的遍历顺序
因为长的回文字符串是由短的回文字符串推导出来的。所以,我们的遍历顺序应该是有短字符串到长字符串。反应到dp数组上面就是如下的遍历顺序:
2、中心扩展算法
该算法其实就是枚举回文子串的中心位置,然后向两边扩散,看最终能扩散多远,然后记录这个位置,在和上一次记录的进行比较,取最长的一个。每次循环的时候需要分奇数和偶数两种情况。这这两种情况其实也只是初始的左右位置不一样,比如遍历到i位置的时候。奇数情况的时候左右起始扩散位置分别就是i - 1, i + 1;而偶数的时候左右起始扩散位置分别就是i , i + 1。
三、参考答案
1、动态规划
class Solution {
public String longestPalindrome(String s) {
//将字符串转换成字符串数组,方便遍历
char[] chars = s.toCharArray();
int length = chars.length,start=0,maxLength=1;
//定义一个二维数组,存储i到j是否是回文字符串
boolean[][] dp = new boolean[length][length];
//从长度为2开始遍历
for(int strLength = 2;strLength<=length;strLength++){
//从头遍历这个字符串,分别统计出当前长度下的各种情况是否是回文字符串,并记录下来
for(int i = 0;i<length;i++){
//当前右边界的位置
int j = i+strLength-1;
//如果右边界超过字符串长度,跳出当前循环
if(j>length-1){
break;
}
//如果左右边界相等
if(chars[i]==chars[j]){
//如果当前步长小于等于3,则一定是回文字符串
if(strLength<=3){
dp[i][j]=true;
}else{ //大于3的时候,是否是回文字符串取决于它的子字符串是否是回文,字符串在之前步长的循环过程中已经得知是///否是回文字符串了
dp[i][j] = dp[i+1][j-1];
}
}else{
dp[i][j]=false;
}
//根据当前遍历出的字符串是否是回文字符串并且当前步长要大于最大回文字符串长度,则更新
if(dp[i][j]&& strLength>maxLength){
start=i;
maxLength = strLength;
}
}
}
return s.substring(start,start+maxLength);
}
}
2、中心扩展算法
class Solution {
public String longestPalindrome(String s) {
char[] chars = s.toCharArray();
int length = chars.length, start = 0, maxLength = 1;
if (length < 2)
return s;
for (int i = 0; i < length; i++) {
// 奇数长度的回文串
int l = i - 1, r = i + 1;
while (l >= 0 && r < length && chars[l] == chars[r]) {
l--;
r++;
}
//当前最大长度小于此时的回文串长度,更新回文串起始位置以及最大回文串长度
if (maxLength < r - l - 1) {
maxLength = r - l - 1;
start = l + 1;
}
// 偶数长度的回文串
l = i;
r = i + 1;
while (l >= 0 && r < length && chars[l] == chars[r]) {
l--;
r++;
}
if (maxLength < r - l - 1) {
maxLength = r - l - 1;
start = l + 1;
}
}
return s.substring(start, start + maxLength);
}
}
在比较这两种算法时,我们会发现无论是从代码复杂度还是执行时间来看,中心扩展算法都表现得比动态规划算法更为出色。因此,对于这个问题,我们更倾向建议使用中心扩展算法。