题目描述
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
示例 3:
输入:s = “a”
输出:“a”
示例 4:
输入:s = “ac”
输出:“a”
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-palindromic-substring
方法1:暴力解法:
就是暴力枚举,每截取到一个字符串就利用回文串判断函数(自己写的)来判断是不是回文串,并记录长度,输出最长的一个就好了 s.subString用起来呀
暴力解法在两个for循环中还做了遍历,因此,时间复杂度是O(n3)
代码展示
class Solution {
public String longestPalindrome(String s) {
if(s.length()==0)
{
return null;
}
if(s.length()==1)
{
return s;
}
int maxLength=0;
//String tempStr=s.charAt(0);//当字符串中没有回文串,就默认返回第一个字符
//String tempStr=s.substring(0,1);
// String tempStr= String.valueOf(s.charAt(0)) ;
// String tempStr = new String(new char[]{s.charAt(0)});
String tempStr = ""+s.charAt(0);
for(int i=0;i<s.length();i++)
{
for(int j=i+1;j<s.length();j++)
{
if(isValidPalindrome(s.substring(i,j+1)))
{
if(j-i+1>maxLength)
{
maxLength=j-i+1;
tempStr=s.substring(i,j+1);//"abcdefgh".substring(3,6);//会截取到"def"
}
}
}
}
return tempStr;
}
boolean isValidPalindrome(String s)
{
int left=0;int right=s.length()-1;
while(left<right)//当字符长度为奇数,如果是回文串,left和right在中间位置相遇,不用比较
{//当字符长度为偶数,如果是回文串,left和right最后一次相当于交换位置,left会大于right
if(s.charAt(left++)!=s.charAt(right--))
{
return false;
}
}
return true;
}
}
bug总结
1:substring的用法
注意,全部都是小写,且substring(i,j)截取到第i到j-1个字符(从第0开始)(j-i代表截取的字符串的长度)
"abcdefgh".substring(3,6);//会截取到"def"
方法接口:
public string substring(int beginIndex,int endIndex)
用于截取字符串
第一个参数指明字符串截取起始位置
第二个参数指明字符串截取终止位置,
最终截取的字符串不包含endIndex位置的字符
当传参数为一个时,表明从传参数位置一直截取到字符串末尾
"abcdefgh".substring(3);//会截取到"defgh"
2:分析:charAt返回一个char类型的字符,不能用String类型接受
解决
1:不用charAt,用substring返回第一个字符
String tempStr=s.substring(0,1);
2 :
String tempStr=Character.toString(s.charAt(0)) ;//使用Character.toString()
String tempStr= String.valueOf(s.charAt(0)) ;//使用String.valueOf()
String tempStr = new String(new char[]{s.charAt(0)});//使用匿名数组
String tempStr = ""+s.charAt(0);//使用连字符
暴力解法改进(参考力扣官方)
对比改进点:
1:s.charAt(i)每次都会检查数组下标是否越界,所以最好先转化成字符数组 char[]arr=s.toCharArray(我觉得主要是这个原因,因为我改成索引模式,上面的方法依旧超时。。。)
2:上面的方法,每次都要截取字符串,保存到tempStr,而改进的方法只是记录了索引,更高效
class Solution {
public String longestPalindrome(String s) {
int len=s.length();
if(len<2)
{
return s;
}
int maxLength=1;
int begin=0;
char [] charArray=s.toCharArray();
for(int i=0;i<s.length()-1;i++)
{
for(int j=i+1;j<s.length();j++)
{
if(j-i+1>maxLength&&isValidPalindrome(charArray,i,j))
{
maxLength=j-i+1;
begin=i;
}
}
}
return s.substring(begin,begin+ maxLength);
}
boolean isValidPalindrome(char[]charArray,int left,int right)
{
while(left<right)//当字符长度为奇数,如果是回文串,left和right在中间位置相遇,不用比较
{//当字符长度为偶数,如果是回文串,left和right最后一次相当于交换位置,left会大于right
if(charArray[left++]!=charArray[right--])
{
return false;
}
}
return true;
}
}
方法2:中心扩散法
枚举所有子串的可能的中心位置
我们知道回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。
中心扩散法:从中间位置开始,向两边扩散,直到不能匹配
需要分奇数长度和偶数长度,因为中心位置可能是一个字符,也有可能是两个相邻的字符;同时需要记录最长回文子串的相关变量
由于存在奇数的字符串和偶数的字符串,所以我们需要从一个字符开始扩展,除了最后一个字符,每一个字符都有两种扩展方法即:
1:以该字符为中心的奇数长度字符串,向两边扩展,直至越界或者s.charAt(left)!=s.charAt(right),
2:以该字符以及它的下一个字符为中心的偶数长度字符串,向两边扩展,直至越界或者s.charAt(left)!=s.charAt(right)
所以总共有 n+n-1 个中心。
我们从给定字符串的最左边开始,依次枚举,比如"babad",先找到第一个字符’b’,它的奇数长度回文串的中心是’b’,当left指针左移就会直接越界,所以,以’b’为中心的奇数长度最长回文子串是"b"。‘b’的偶数长度回文串的中心是’ba’,当left指针左移就会直接越界,以’ba’为中心的偶数长度最长回文子串是"ba"
我们每次记录得到的回文字符串的长度,先判断这个长度是奇数还是偶数,然后在根据中间索引和回文字符串的长度判断这个回文字符串的开始索引,最后返回最长的回文字符串
相比暴力解法,中心扩散法每次直接找到以这个字符为中心的最长回文串,而不会把它的所有回文串都求出来,更节约时间
时间复杂度:O(n²)
空间复杂度:O(1)
class Solution {
public String longestPalindrome(String s) {
if(s.length()==0)
{
return null;
}
if(s.length()==1)
{
return s;
}
int maxLength=1;
int startIndex=0;//回文字符串的开始索引
for(int i=0;i<s.length();i++)//把每一个字符的当做中心字符,看看以它为中心的回文字符串的最长长度
{
int length1=expandFromMid(s,i,i);//回文字符串长度为奇数,中心为s.charAt(i)
int length2=expandFromMid(s,i,i+1);//回文字符串长度为偶数,中心为s.charAt(i)和s.charAt(i+1)
if(length1>maxLength)
{
maxLength=length1;
startIndex=i-maxLength/2;//因为是从中间向两边扩散的嘛,
//我们根据返回来的回文字符串长度以及原来的中心位置,来确定回文字符串的开始索引
}
if(length2>maxLength)
{
maxLength=length2;
startIndex=i-(maxLength-2)/2;
}
}
return s.substring(startIndex,startIndex+maxLength);
}
int expandFromMid(String s,int left,int right)
{
while(left>=0&&right<s.length())//向中心字符两边扩散,且防止越界
{
if(s.charAt(left)==s.charAt(right))
{
left--;//向左扩散
right++;//向右扩散
}
else
{
break;
}
}
return right-left-1;//循环结束,返回以当前字符为中心的最长回文串的长度
//注意,此时left和right所指向的字符不相等,
//所以,回文字符串的长度为right-left+1-2=right-left-1
}
}
中心扩散法(力扣官方)
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) {
return "";
}
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
public int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
--left;
++right;
}
return right - left - 1;
}
}
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
对比:主要还是求最长回文字符串的开始索引有所不同
官方题解是每次更新可能的最长回文字符串的开始索引start和结束索引end,而我的方法是每次更新,可能的最长回文字符串的开始索引startIndex和长度。我的方法每次更新需要比较两次,可能更耗时间
方法3:动态规划
一个回文去掉两头以后,剩下的部分依然是回文。
如果一个字符串两端的字符不相等,那么不是回文字符串,如果相等,那么需要判断中间的字符串是不是回文字符串
状态:dp[i][j]表示子串s [i…j]是否为回文子串
得到状态转移方程:dp[i][j] = (s[i] == s[j] ) and dp[i + 1][j - 1],当s[i] = s[j] ,子串的回文性质决定了整体的回文性质
边界条件:j - 1 - (i + 1) + 1 < 2,(j-1和i+1是转移方程的下标)整理得j - i<3
动态规划比暴力解法快,利用动态转移方程快速找到一个子串是否是回文串,尽可能地利用了之前计算的结果,非常典型的空间换时间的算法思想,因为单个字符一定是回文串,因此dp[i][i]可以初始化为true,然而,初始化的部分是可以省略的,因为我们真正执行代码就会发现,对角线上的数值不会被其他状态所参考
输出:在得到一个状态的值为true的时候,记录起始位置和长度,填表完成以后再截取子串
由于dp[i][j]参考它左下方的值:dp[i+1][j-1],所以,我们不能一行一行地填表,即要拿到某一个单元格的值,要么它自身的下标差值小于3,可以进行判定是否是回文串,当它的下标差值大于等于3,则需要先知道它的左下方的值
参考:
(1)先升序填列;
(2〉再升序填行。
因为单个字符肯定是回文串,所以,我们先把对角线dp[i][i]置为true.由于i肯定是小于j的,所以,我们只会用到上半部分表格
现在按列填写,先填写第二列
填第三列
填第四列
填最后一列
动态规划从一个比较小的规模的问题开始,逐步得到一个比较大的问题的结果的过程,并且在这个过程中记录每一步的结果
代码
class Solution {
public String longestPalindrome(String s) {
int length=s.length();
if(length==0)
{
return null;
}
if(length==1)
{
return s;
}
int maxLength=1;
int startIndex=0;//回文字符串的开始索引
char[] arr=s.toCharArray();
boolean dp[][]=new boolean[length][length];
for(int i=0;i<length;i++)
{
dp[i][i]=true;//为了结果的合理性,虽然对角线元素不会被参考,但还是置为true
}
for(int j=1;j<length;j++)//第0列已经确定了,所以从第一列开始赋值
{
for(int i=0;i<length;i++)//注意内层循环是行下标
{
if(arr[i]!=arr[j])
{
dp[i][j]=false;
}
else{
if(j-i<3)//说明是包含单个字符或者两个字符或者3个字符的子串,当arr[i]=arr[j],它们都是回文串
{
dp[i][j]=true;
}
else{
dp[i][j]=dp[i+1][j-1];
}
}
if(dp[i][j]&&j-i+1>maxLength)//如果dp[i][j]是回文串,且长度大于当前最大长度,就更新 maxLength以及最长回文串开始索引
{
maxLength=j-i+1;
startIndex=i;
}
}
}
return s.substring(startIndex,startIndex+maxLength);
}
}
力扣官方的动态规划
public String longestPalindrome(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n];
String ans = "";
for (int l = 0; l < n; ++l) {
for (int i = 0; i + l < n; ++i) {
int j = i + l;
if (l == 0) {
dp[i][j] = true;
} else if (l == 1) {
dp[i][j] = (s.charAt(i) == s.charAt(j));
} else {
dp[i][j] = (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]);
}
if (dp[i][j] && l + 1 > ans.length()) {
ans = s.substring(i, i + l + 1);
}
}
}
return ans;
}
}
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
方法 4 :马拉车(Manacher)算法
这个算法就暂时不研究了,先贴上力扣官方的代码
class Solution {
public String longestPalindrome(String s) {
int start = 0, end = -1;
StringBuffer t = new StringBuffer("#");
for (int i = 0; i < s.length(); ++i) {
t.append(s.charAt(i));
t.append('#');
}
t.append('#');
s = t.toString();
List<Integer> arm_len = new ArrayList<Integer>();
int right = -1, j = -1;
for (int i = 0; i < s.length(); ++i) {
int cur_arm_len;
if (right >= i) {
int i_sym = j * 2 - i;
int min_arm_len = Math.min(arm_len.get(i_sym), right - i);
cur_arm_len = expand(s, i - min_arm_len, i + min_arm_len);
} else {
cur_arm_len = expand(s, i, i);
}
arm_len.add(cur_arm_len);
if (i + cur_arm_len > right) {
j = i;
right = i + cur_arm_len;
}
if (cur_arm_len * 2 + 1 > end - start) {
start = i - cur_arm_len;
end = i + cur_arm_len;
}
}
StringBuffer ans = new StringBuffer();
for (int i = start; i <= end; ++i) {
if (s.charAt(i) != '#') {
ans.append(s.charAt(i));
}
}
return ans.toString();
}
public int expand(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
--left;
++right;
}
return (right - left - 2) / 2;
}
}
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。