最长回文字符串
leetcode第五题。
Description:
Longest Palindromic Substring
Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
求最大回文字符串子集问题。
分析:
首先确定什么是有效解:
对于一个合法的回文字符串,一定满足: A[i]=A[n-i-1],i=start~end, n=end-start;
a) 暴力枚举
从这个定义上,可以用嵌套循环枚举所有的子字符串,然后循环判断该子字符串是否是合法的回文字符串。很容易得出一个暴力方法:
function sol1(s):
n=s.size;
int[][] isPalin;
int maxlen=0;
int maxi,maxj;
for i=1 to n:
for j=i+1 to n:
//判断(i,j)是否是合法的回文字符串
isPalin[i][j]=isPalindromic(s,i,j);
if isPalin[i][j]==1 then:
if (j-i+1)>maxlen then:
maxlen=j-i+1;
maxi=i;
maxj=j;
end if;
end for
end for
return s.substring(maxi,maxj);
end function;
function isPalindromic(s,i,j):
for k=i to j:
if s[k]!=A[j-k-1] then:
//不是回文字符串
return 0;
end if;
end for
return 1;
end function;
正确性分析:
枚举了所有子字符串,逐个判断比较取最大,显然是正确的。
时间复杂度分析:
用了三层循环,时间复杂度为:O(n^3).
暴力求解方法的时间复杂度显然过高。
b) 改善
观察回文字符串,有两种情况:
-
如果字符串长度为奇数,以中间字符串为轴两边的字符串都相等
“bab”
s[mid-i]=s[mid+i], mid:回文字符串的中间位置,i=1~mid, mid=(start+end)/2
-
如果字符串长度为偶数,lmid,rmid为左中间位置,右中间位置。
“abba”
s[lmid-i]=s[rmid+i], lmid=(start+end)/2,rmid=(start+end+1)/2, i=1~lmid.
只有确保以上才是一个合法的回文字符串,同时根据停止的i的位置,可以得出
所以我们可以写一个循环判断每个字符是否是回文字符串的中间字符:
function sol2(s):
n=s.length();
if n==0 then:
return "";
end if;
maxs =s.substr(0,1);
for(int i=0,i<n-1;i++){
p1=maxMidOfPlain(s,i,i);
if p1.length() > maxs.length then:
maxs=p1;
end if;
p2=maxMidOfPlain(s,i,i+1);
if p2.length()>maxs.length() then:
maxs=p2;
end if;
}
return maxs;
end function;
function maxMidOfPlain(s,c1,c2):
int left=c1,right=c2;
int n=s.length();
while(left>=0 && right<=n-1 &&s[left]==s[right]):
left--;
right++;
end while;
return s.substring(l+1,r-l-1);
end function;
cpp代码:
class Solution {
public:
string longestPalindrome(string s) {
int n=s.length();
if(n==0)
return "";
string maxs=s.substr(0,1);
for (int i = 0; i < n; ++i) {
string p1=maxMidOfPlain(s,i,i);
if (p1.length()>maxs.length())
maxs=p1;
string p2=maxMidOfPlain(s,i,i+1);
if (p2.length()>maxs.length())
maxs=p2;
}
return maxs;
}
string maxMidOfPlain(string s, int c1,int c2){
int left=c1,right=c2;
int n=s.length();
while (left>=0 && right <=n-1 && s[left]==s[right]){
left--;
right++;
}
return s.substr(left+1,right-left-1);
}
};
以上代码Runtime: 12 ms
还可以继续优化达到4 ms的时间。
class Solution {
public:
string longestPalindrome(string s) {
int n=s.length();
if(n<2)
return s;
string maxs=s.substr(0,1);
int max_left=0,max_len=1,left,right;
for (int i = 0; i < n && n-i>max_len/2;) {
left=right=i;
while (right<n-1 && s[right + 1]==s[right])
++right; //消除重复元素
i=right+1;
while(left>0&&right<n-1&&s[right+1]==s[left-1]){
--left;
++right;
}
if (max_len<right-left+1){
max_left=left;
max_len=right-left+1;
}
}
return s.substr(max_left,max_len);
}
};
我觉得优化主要是消除了重复元素的影响,不再调用函数,减少调用函数时入栈出栈的消耗,但是效果出奇的好。运行时间只有4 ms。
正确性分析:
略
时间复杂度分析:
使用了两个for循环,时间复杂度为: O(n^2)
c) DP(动态规划)求解
分析: 对于区间[i,j]的substring,如果它是回文字符串,那么区间[i+1,j-1]的substring也一定是一个回文字符串。
基于以上的结构,我们可以得出最优子结构:
o
p
t
[
i
,
j
]
=
{
t
r
u
e
,
i
f
 
o
p
t
[
i
+
1
,
j
−
1
]
=
t
r
u
e
a
n
d
s
[
i
]
=
s
[
j
]
f
a
l
s
e
,
o
t
h
e
r
w
i
s
e
opt[i,j]=\begin{cases} true, &&if \, opt[i+1,j-1]=true&and &s[i]=s[j] \\ false,&&otherwise \end{cases}
opt[i,j]={true,false,ifopt[i+1,j−1]=trueotherwiseands[i]=s[j]
根据这个最优子结构,使用两层for循环,枚举所有可能的substring,不同于暴力解法的是,这里判断substring是否为合法的回文字符串时,只需要要check,opt[i+1,j-1],和s[i]=s[j].
可以改善空间复杂度,将二维数组换成两个一维数组。只存储前一排的值即可。
伪代码:
function sol3(s):
n=s.size;
int[][] opt=0; //初始值
int[] oldArr;
for i=1 to n:
opt[i][i]=1;
if s[i]==s[i+1] then:
opt[i][i+1]=1;
maxi=i;
maxj=i+1;
end if;
end for;
for i=1 to n:
for j=i+2 to n:
if opt[i+1][j-1]==1 and s[i]==s[j] then:
opt[i][j]=1;
if j-i+1>maxlen then:
maxlen=j-i+1;
maxi=i;
maxj=j;
end if;
end if;
end for;
end for;
return s.substring(maxi,maxj);
end function;
cpp代码:
class Solution {
public:
string longestPalindrome(string s) {
int n=s.length();
if(n==0){
return "";
}
bool opt[1000][1000]={false};
int maxlen=1;
int maxi=0;
for (int i = 0; i < n; i++) {
opt[i][i]= 1;
}
for (int i = 0; i < n-1; i++) {
if (s[i]==s[i+1]){
opt[i][i+1]=1;
maxi=i;
maxlen=2;
}
}
for (int len = 3; len <=n ; len++) {
for (int i = 0; i < n-len+1; i++) {
int j=i+len-1;
if (s[i]==s[j]&&opt[i+1][j-1]){
opt[i][j]= true;
maxi=i;
maxlen=len;
}
}
}
return s.substr(maxi,maxlen);
}
};
正确性分析:
最优子结构式子没问题,代码遵照最优子结构式子,那么正确性应该得以保证。
复杂度分析:
使用了两层for循环,时间复杂度为: O ( n 2 ) O(n^2) O(n2), 空间复杂度: O ( n 2 ) O(n^2) O(n2),可以改善空间复杂度,将二维数组换成两个一维数组
d)Manacher’s algorithm
这个non-trivial的算法,很多博客上都有介绍,想在短时间内想出一个这么好的算法应该不容易,但这是一个线性时间的算法,
该算法具体实现思想好多博客说的也比较清楚,开头放的链接里也有介绍。
- 对字符串进行预处理。将字符间插入#,字符开头插入$,字符结尾插入^
- 开辟一个数组P,其中P[i]记录的是以i为中心的最长回文字符串的长度。R记录最长右边界,C记录最长回文字符串的中心位置
- 将i和R向右移动找到最大回文字符串。
class Solution {
public:
string longestPalindrome(string s) {
int n=s.length();
if(n<2)
return s;
string T="^";
for (int i = 0; i < n; i++) {
T+="#"+s.substr(i,1);
}
T+="#$";
n=T.length();
int *P=new int[n];
int C=0, R=0;
for (int i = 1; i < n-1; i++) {
int i_mirror=2*C-i;
P[i]=(R>i)?min(R-i, P[i_mirror]):0;
while (T[i+1+P[i]]==T[i-1-P[i]])
P[i]++;
if(i+P[i]>R){
C=i;
R=i+P[i];
}
}
int maxLen = 0;
int centerIndex = 0;
for (int i = 1; i < n-1; i++) {
if (P[i] > maxLen) {
maxLen = P[i];
centerIndex = i;
}
}
delete[] P;
return s.substr((centerIndex - 1 - maxLen)/2, maxLen);
}
};
正确性分析:
略。
时间复杂度分析:
使用了两个for循环,其中第一个for循环内嵌套了一个while循环,乍看会觉得是 O ( n 2 ) O(n^2) O(n2)的复杂度,其实并不是,这个while循环只是处理,匹配越界的后续匹配问题,其本质上是右边界拓展,整个字符串扩展R最多也就需要N步。故时间复杂度仍然是 O ( n ) O(n) O(n).