题目大意:给定一个字符串s,找到其最长的回文子串,假设s的长度最长为1000,并且存在一个唯一的最长子串。返回最长的子串。
题目意思挺清楚的,但是需要考虑多种情况,分别有:
1、s为一个字符,此时s即为所求的回文子串。假定此时刻求出的最长子串长度为max。
2、s中含有“ommo”最常长子串,此时设i指向第一个m,需要判定当前字符和下一个字符是否相同,即s[m]和s[m+1],若相同,则判断s[m-1]和s[m+2],直到不等,求出最长子串长度len1。接着判断s[m-1]和s[m+1]是否相同,直到不等,求出最长子串长度len2。
3、若当前字符和下一个字符不相同,则令len1=0,判断s[m-1]和s[m+1]是否相同,直到不等,求出最长子串长度len2。
4、对len1和len2取最大值,如果大于max,则更新max,并将此时得最长子串赋值给结果字符串resu。
需要注意的是:不管当前字符和下一个字符是否相同,都要进行一下操作:判断s[m-1]和s[m+1]是否相同,直到不等,求出最长子串长度len2。
原因是:s为“baaab”,当执行到i=2指向第一个a时,s[m]和s[m+1]都为a,求得len1=2,然而并不是最长的。继续判断s[m-1]和s[m+1]相同,s[m-2]和s[m+2]相同,得len2=5。所以这点需要注意。
下面是提交ac的代码,测试运行时间为44ms:
#include "stdafx.h"
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
class Solution {
public:
string longestPalindrome(string s) {
int i,j;
string resu="";
if(s.length()==1)
return s;
int max=1,len1=0,len2=0,len=0;
for(int i=0;i<s.length()-max/2;++i)
{
len1=0,len2=0;
if(s[i]==s[i+1]) //若当前和下一个相同
{
j=1;
while((i-j)>=0&&(i+j+1)<s.length()&&s[i-j]==s[i+j+1])
++j;
len1=2*j;
}
{ //当前的第下j个和第上j个,<span style="font-size:14px;">s[m-1]和s[m+1]...</span>
j=0;
while((i-j)>=0&&(i+j)<s.length()&&s[i-j]==s[i+j])
++j;
len2=2*j-1;
}
if(len1>len2)
len=len1;
else
len=len2; //取最大
if(len>max) //当前较大,更新resu
{
max=len;
resu.clear();
resu.insert(0,s,i-(len+1)/2+1,len);
}
}
return resu;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
string ss="bb";
Solution so;
string rr=so.longestPalindrome(ss);
cout<<rr<<endl;
return 0;
}
看统计的运行时间,发现很多能在10ms以下解决,上述思路的时间复杂度为O(N^2),i需要循环整个字符串,对每个s[i]需要往两边扫,直到不相同。唯一进行优化了的措施是,i循环次数为s.length()-max/2,减少了一部分。没想到更好的算法,直接看了讨论区的最佳算法。
代码如下:
string longestPalindrome(string s) {
if (s.empty()) return "";
if (s.size() == 1) return s;
int min_start = 0, max_len = 1;
for (int i = 0; i < s.size();) {
if (s.size() - i <= max_len / 2) break;
int j = i, k = i;
while (k < s.size()-1 && s[k+1] == s[k]) ++k; // Skip duplicate characters.
i = k+1;
while (k < s.size()-1 && j > 0 && s[k + 1] == s[j - 1]) { ++k; --j; } // Expand.
int new_len = k - j + 1;
if (new_len > max_len) { min_start = j; max_len = new_len; }
}
return s.substr(min_start, max_len);
}
1、第6行,即将末尾不需要判断的舍去,上述我也用到了。
2、对于重复元素,直接跳过,比较重复元素块两端的扩展元素是否相同。原因是重复元素无论为多少个,都是满足回文要求的,并且跳过直接判断两端开始,得到的一定是最大长度。此时就可以跳到重复元素块最后位置的下一个位置进行判断了。
例:abcdeeeeeedcmn,当执行到i=4时,一直跳到比较s[i-1]和s[i+6],s[i-2]和s[i+7]等,得到此时得长度max,此时i=5-9都不用计算了,直接跳到i=10。回文子串中,这种情况是常出现的,用自己的方法,有重复元素时,需要执行两个while循环,耗时较多。因此此优化可以大大减小运行时间。
3、因为有判断重复元素,因此不必将回文子串分为奇数和偶数进行处理了,处理更方便。
此外,还有一种O(n)的算法,Manacher算法,参见下面的链接,解释得比较详细:
[译+改]最长回文子串(Longest Palindromic Substring) Part II
2016.09.02更新 JAVA代码
manacher算法,一直拖着没看,今天花时间理解了一番,提交代码如下:
public class Solution {
public String longestPalindrome(String s) {
int len = s.length();
char[] str = new char[2 * len + 1];
for (int i = 0; i < len; i++) {
str[2 * i] = '#';
str[2 * i + 1] = s.charAt(i);
}
str[2 * len] = '#'; //添加额外字符奇数偶数统一处理
int[] parr = new int[str.length];
int pr = -1;
int index = -1;
int max = Integer.MIN_VALUE;
int maxIndex = -1;
for (int i = 0; i < str.length; i++) {
parr[i] = pr > i ? Math.min(parr[2 * index - i], pr - i) : 1; //四种情况 两种直接得到值
while (i + parr[i] < str.length && i - parr[i] > -1) { //两种需进行while循环处理
if (str[i + parr[i]] == str[i - parr[i]]) { //直接得到值得不进入while
parr[i]++;
} else {
break;
}
}
if (i + parr[i] > pr) {
pr = i + parr[i]; //更新能到达的最大位置和下标
index = i;
}
if (parr[i] > max) { //更新最大时的长度和下标
max = parr[i];
maxIndex = i;
}
}
char[] res = new char[parr[maxIndex] - 1]; //最大长度时的下标进行子串恢复
int start = maxIndex - parr[maxIndex] + 1;
for (int i = 0; i < res.length; i++) {
res[i] = str[start + 2 * i + 1];
}
return String.valueOf(res);
}
}