问题描述
给定一个字符串 s,找到 s 中最长的回文子串
回文是指左右对称的字符串
问题分析
1.暴力搜索
如同第三题最长无重复子串,第一种容易想到的思路就是依次求以字符数组中每个字符开头的最长回文子串,再从中选取最长的那个作为返回结果,如下图所示,数字表示判断的顺序
如何判断是不是回文串呢?根据回文串的对称特征可知,左右两边的字符都是相同的。使用双指针法,分别指向子串的起始和终止,若相等则指针各自向内收缩一个字符继续判断,如图所示,直到结束或者出现了不相同的字符为止,先给出回文串判断的函数
public static boolean IsPalindrome(String s, int start, int end) {
if(start > end)
return false;
//注意:这里不用带等号
while(start < end) {
if(s.charAt(start) != s.charAt(end))
return false;
start ++;
end --;
}
return true;
}
如上所述,遍历字符数组,得出最长回文串。在这里需要返回的是字符串而不是长度,因此需要额外的一个变量记录该串的起始位置,终止位置蕴含在了最大长度中,代码如下所示
public String longestPalindrome(String s) {
int strLen = s.length();
if(strLen == 0)
return s;
//至少有一个字符构成回文串
int maxLen = 1;
//起始位置
int begin = 0;
//遍历数组
for(int i = 0; i < strLen; i++){
//构建子串
for(int j = i + 1; j < strLen; j++){
//回文判断
if(j - i + 1 > maxLen && IsPalindrome(s,i,j)){
int curLen = j- i + 1;
if(curLen > maxLen) {
maxLen = curLen;
//在这里修改起始位置
begin = i;
}
}
}
}
return new String(s.toCharArray(), begin, maxLen);
}
这里需要注意一下这一句
if(j - i + 1 > maxLen && IsPalindrome(s,i,j))
因为是要找最长的回文串,要是这个串本身的长度比目前的最大长度还小,那就不用再判断是不是回文了
结果
遍历数组需要O(n),构建子串需要O(n), 回文串判断需要O(n),整体的时间复杂度为O(n^3)
2.中心扩张
解法1中,根据回文特征,由外向内收缩检查是不是回文。相反的,可以选取一个点(叫中心点),由内向外扩张判断是不是回文,在这里需要注意一下奇数型和偶数型的问题:
如果一个字符两边的字符都相等,说明能构成一个基本型回文(奇数型)如:bab
如果一个字符和右边的字符相等,且这两个相等字符的左右两边的字符还是相等的,则能构成一个基本型回文(偶数型)如baab
当我们得到回文子串的基本型之后,就可以向两边扩张来试图得到更大的回文串,代码如下
public String longestPalindrome(String s) {
if(s == null || s.length() == 0)
return s;
int strLen = s.length();
int curLen = 1,maxLen = 1;
int begin = 0;
for(int i = 0; i < strLen; i++){
//注意:防止越界
if(i - 1 >= 0 && i + 1 < strLen && s.charAt(i - 1) == s.charAt(i + 1)){
//奇数型
int left = i - 1, right = i + 1;
for(; left >= 0 && right < strLen; left --, right ++ ){
if(s.charAt(right) != s.charAt(left)){
break;
}
}
curLen = right - left - 1;
if(curLen > maxLen){
begin = left + 1;
maxLen = curLen;
}
}
//注意:防止越界
if( i + 1 < strLen && s.charAt(i) == s.charAt(i + 1)) {
//偶数型
int left = i - 1,right = i + 2;
for(; left >= 0 && right < strLen; left --, right ++){
if(s.charAt(right) != s.charAt(left)){
break;
}
}
//注意:break到这里说明左右不相等,要向内收缩两个字符,所以长度还要减一
curLen = right - left - 1;
if(curLen > maxLen){
begin = left + 1;
maxLen = curLen;
}
}
}
return new String(s.toCharArray(), begin, maxLen);
}
需要注意的点:
1. 回文基本型判断时,要注意增加边界条件检查,防止数组越界,在注释中已标注
2. 长度计算要格外注意
结果
遍历数组为O(n),扩张操作为O(n) ,故整体复杂度为O(n^2)。可以看出,与暴力搜索法先构建子串再判断回文相比,中心扩张法在扩张的过程中,同时完成了子串构建和回文判断,因此复杂度降低了一个幂
可以看出本该代码从工程角度并不优美,奇数型和偶数型使用了相同逻辑的扩张代码。有重复的代码出现,考虑重构一下
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;
String res = s.substring(0, 1);
for (int i = 0; i < len - 1; i++) {
//奇数型判断
String oddStr = centerSpread(s, i, i);
//偶数型判断
String evenStr = centerSpread(s, i, i + 1);
//哪种更长选哪个
String maxLenStr = oddStr.length() > evenStr.length() ? oddStr : evenStr;
//
if (maxLenStr.length() > maxLen) {
maxLen = maxLenStr.length();
res = maxLenStr;
}
}
return res;
}
private String centerSpread(String s, int left, int right) {
int len = s.length();
int i = left;
int j = right;
while (i >= 0 && j < len) {
if (s.charAt(i) == s.charAt(j)) {
i--;
j++;
} else {
break;
}
}
// 此时左右不等,因此向内收缩,substring的区间是左闭右开的
return s.substring(i + 1, j);
}
3.动态规划
动态规划的主要任务是先给出一个问题模型(父问题),然后试图将其拆分成同等性质的更小规模的问题(子问题)。通常父问题的解不是子问题解的简单相加,而是有一定的关系,找出这个关系就比较容易得到动态规划的解法
先将问题简化为是不是回文串的问题,假设
f
(
s
,
0
,
l
)
f(s,0,l)
f(s,0,l)用于判断字符串从
0
0
0 到
l
l
l 的部分是不是回文
假设我们已知
f
(
s
,
1
,
l
−
1
)
f(s, 1, l -1)
f(s,1,l−1)是
t
r
u
e
true
true,则只需要判
s
[
0
]
s[0]
s[0]和
s
[
l
]
s[l]
s[l]是不是相等字符,则有
f
(
s
,
i
,
j
)
=
{
t
r
u
e
,
f
(
s
,
i
+
1
,
j
−
1
)
=
=
t
r
u
e
且
s
[
i
]
=
=
s
[
j
]
f
a
l
s
e
,
e
l
s
e
f(s,i,j)= \begin{cases} true, f(s,i + 1, j - 1) == true 且 s[i] == s[j]\\ false, else \end{cases}
f(s,i,j)={true,f(s,i+1,j−1)==true且s[i]==s[j]false,else
如何将相同的思考方式运用到求最长回文串呢?未完待续…