5. 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.
Example:
Input: "babad" Output: "bab" Note: "aba" is also a valid answer.
Example:
Input: "cbbd" Output: "bb"
思路:
解法1:暴力法。找出所有字串(O(n^2)),再判断一个字串是否为回文串(O(n)),故总的时间复杂度为O(n^3)
解法2:动态规划。记p(i, j)为字符串s中从下标i到j的字串,p(i, j)等于true代表其为回文串,等于false代表不是回文串,故:
1、一定有:p(i, i)=true;
2、if(s[i]==s[i+1])则有:p(i, i+1)=true;//用于初始化数组的条件
3、if(p(i+1, j-1)==true && s[i]==s[j])则有:p(i, j)=true;//用于递推的条件
以字符串babad为例,求解的过程就是填完一个5x5的数组的过程:
| b | a | b | a | d | |
| b | 1 | 0 | x | y | z |
| a | 1 | 0 | x | y | |
| b | 1 | 0 | x | ||
| a | 1 | 0 | |||
| d | 1 |
注意填的顺序为:表中的1和0是初始化的时候填的,然后填图中值以x代替的元素,然后是y,最后是z。实际上只用到了矩阵的右上半部分。
时间复杂度为O(n^2),空间复杂度为O(n^2),代码如下:
1 class Solution { 2 public: 3 string longestPalindrome(string s) { 4 if(s.size()==0)return s; 5 string str; 6 bool arr[2000][2000]; 7 int start=0;//记录下最长的回文串的开始位置 8 int length=0;//记录下最长的长度 9 for(int i=0; i<s.size(); ++i)//初始化 10 { 11 arr[i][i]=true; 12 if(i+1<s.size() && s[i]==s[i+1]) 13 { 14 arr[i][i+1]=true; 15 start=i; 16 length=2; 17 } 18 else arr[i][i+1]=false; 19 } 20 for(int offset=2; offset<s.size(); ++offset)//offset为偏移 21 { 22 for(int i=0; i<s.size()-offset; ++i) 23 { 24 int j=i+offset; 25 if(arr[i+1][j-1] && s[i]==s[j]) 26 { 27 arr[i][j]=true; 28 start=i; 29 length=j-i+1; 30 } 31 else arr[i][j]=false; 32 } 33 } 34 if(length>0)str=s.substr(start, length); 35 else str=s.substr(start, 1); 36 return str; 37 } 38 };
解法3:中心拓展法。时间复杂度为O(n^2),空间复杂度为O(1)。基本思想是回文串字符可从中间向两边拓展,分为长度为奇数和偶数两种情况,比如:
1、aba //长度为奇数
2、abba //长度为偶数
1 class Solution { 2 public: 3 string longestPalindrome(string s) { 4 if(s.size()==0)return s; 5 string str; 6 int start=0; 7 int length=0; 8 for(int idx=0; idx<s.size(); ++idx)//长度为奇数 9 { 10 int left=idx-1; 11 int right=idx+1; 12 while(left>=0 && right<s.size() && s[left]==s[right]) 13 { 14 --left; 15 ++right; 16 } 17 if(right-left-1>length) 18 { 19 length=right-left-1; 20 start=left+1; 21 } 22 } 23 for(int idx=0; idx<s.size(); ++idx)//长度为偶数 24 { 25 int left=idx; 26 int right=idx+1; 27 while(left>=0 && right<s.size() && s[left]==s[right]) 28 { 29 --left; 30 ++right; 31 } 32 if(right-left>length) 33 { 34 length=right-left-1; 35 start=left+1; 36 } 37 } 38 if(length>=2)str=s.substr(start, length); 39 else str=s.substr(0, 1); 40 return str; 41 } 42 };
解法4:Manacher法。时间复杂度为O(n)。
网上找了很多博客,看了半天才看明白,在这里推荐两篇我认为讲得最清楚的博客:https://www.felix021.com/blog/read.php?2040、http://blog.csdn.net/pi9nc/article/details/9251455
我在这里也简单提几点:
1、Manacher法首先通过插入特殊字符来统一回文串为奇数和偶数的情况,以字串abaaba为例,可将其化为#a#b#a#a#b#a#,这个都很好理解。
2、Manacher法的精髓就是利用回文串类似镜像的特点,借助已有的回文串为下一个回文串的查找提供帮助,以此来降低时间复杂度。先来搞懂几个参数的意义:
p[i]: 在字符串s中,以s[i]为中心的最大回文串的半径
id: 已知的 {右边界最大} 的回文子串的中心
mx: mx=p[id]+id,也就是以id为中心的最大回文串的右边界
j: j=2*id-i,也就是j=id-(i-id),即j是i关于id的对称点
可以得到一个重要的结论:如果mx>i,则有p[i]>=min(p[j], mx-i)。嗯,降低时间复杂度就靠它了,下面参照下图来解释这个结论,这个结论说明,如果i落在以id为中心的回文串的范围之内(即条件mx>i),那么我们可以在id左边找到一个i的对称点j,p[j]我们是已知的,则如果p[j]<mx-i,则必有p[i]=p[j],如果p[j]>=mx-i,那么一定有p[i]>=mx-i。根据i落在以id为中心的大的回文串之内很容易推知。
综上:如果mx>i,则有p[i]>=min(p[j], mx-i),其中j=2*id-i,为i关于id的对称点
图1:p[j]<mx-i

图2:p[j]>=mx-i

最后,关于时间复杂度,说实话我还真不是太理解为什么这种算法的时间复杂度是O(n),好像一眼看不出来,毕竟虽然可以用已有回文串的镜像性来为下一个回文串的查找提供一定的帮助,但是当mx<i或者是p[j]>=mx-i的时候,还是要用到中心拓展法来找,貌似时间复杂度介于O(n)和O(n^2)之间,总觉得至少也会大于O(n),好吧,这里先存疑,以后再慢慢想。代码如下:
1 class Solution { 2 public: 3 int min(int a, int b) 4 { 5 if(a<b)return a; 6 else return b; 7 } 8 string longestPalindrome(string s) { 9 if(s.size()==0)return s; 10 char *str=new char[2*s.size()+1]; 11 for(int i=0, j=0; i<s.size(); i=i+1, j=j+2) 12 { 13 str[j]='#'; 14 str[j+1]=s[i]; 15 } 16 str[2*s.size()]='#';//化为#a#b#a#a#b#a#的形式 17 int *p=new int[2*s.size()+1]; 18 p[0]=1;//初始化为1 19 int id=0; 20 for(int i=1; i<2*s.size()+1; ++i) 21 { 22 int mx=p[id]+id; 23 if(mx>i)//真正降低时间复杂度的部分 24 { 25 int j=2*id-i; 26 p[i]=min(p[j], mx-i); 27 }else{ 28 p[i]=1; 29 } 30 int left=i-p[i]; 31 int right=i+p[i]; 32 while(left>=0 && right<2*s.size()+1 && str[left]==str[right])//在前面的基础上,从中心往两边拓展,尽可能地拓展回文串 33 { 34 --left; 35 ++right; 36 } 37 p[i]=(right-left)/2;//更新拓展过后的p[i] 38 if(p[i]+i>mx)id=i;//更新id,如果没有这一句,整个代码退化为中心拓展法的代码 39 } 40 int center=0; 41 for(int idx=0; idx<2*s.size()+1; ++idx)//用center记录下半径最长的中心位置 42 { 43 if(p[idx]>p[center])center=idx; 44 } 45 int start=(center-p[center]+1)/2;//算出在原始字串中的起始位置和长度 46 int length=p[center]-1; 47 delete []str; 48 delete []p; 49 return s.substr(start, length); 50 } 51 };
最后,以字符串abaaba为例,以下是代码执行结果(在上面的代码中加入了一些打印信息):


浙公网安备 33010602011771号