题目描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
- 输入: “babad”
- 输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
- 输入: “cbbd”
- 输出: “bb”
解题思路
1、暴力解
最容易想到的就是暴力解,求出原字符串s的每一个子串,然后再判断是不是回文,找到最长的那个即可。
class Solution {
//判断字符串是否为回文
public static boolean judge(String s){
StringBuffer sb = new StringBuffer(s);
sb.reverse();
String str = new String(sb);
if(s.equals(str)){
return true;
}
return false;
}
public String longestPalindrome(String s){
int l = s.length();
//如果字符串长度小于2,即返回该字符串
if(l < 2){
return s;
}
//通过两个for循环获取每个子串并判断是否为回文
//这里由最长子串到最短子串,因此当遇到第一个子串为回文,即是最长回文子串
for(int i = l; i > 0; i--){
for(int j = 0; j <= l - i; j++){
String str = s.substring(j, j + i);
if(judge(str)){
return str;
}
}
}
return s;
}
}
求每一个子串通过两个for循环实现,时间复杂度为 O ( N 2 ) O(N^2) O(N2),判断子串是否为回文的时间复杂度为 O ( N ) O(N) O(N),因此暴力解的时间复杂度为 O ( N 3 ) O(N^3) O(N3),此算法的复杂度太高,很显然不会让面试官满意
2、动态规划
对于字符串
S
S
S ,假设
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示字符串
S
S
S 下标从
i
i
i 到
j
j
j 的子串是否是回文子串
- 如果 f [ i ] [ j ] f[i][j] f[i][j] 是回文,那么 f [ i + 1 ] [ j − 1 ] f[i+1][j-1] f[i+1][j−1] 一定是回文子串
- 如果 f [ i + 1 ] [ j − 1 ] f[i+1][j-1] f[i+1][j−1] 不是回文,那么 f [ i ] [ j ] f[i][j] f[i][j] 一定不是回文子串
那么最长回文子串就能分解成一系列子问题,可以利用动态规划求解了。由此我们可以得到动态规划的递归方程:
f [ i ] [ j ] = { f [ i + 1 ] [ j − 1 ] , S [ i ] = S [ j ] f a l s e , S [ i ] 不 等 于 S [ j ] f[i][j]=\begin{cases} f[i+1][j-1] &,S[i]=S[j] \\ false &,S[i] 不等于 S[j] \\ \end{cases} f[i][j]={f[i+1][j−1]false,S[i]=S[j],S[i]不等于S[j]
初始状态
{ f [ i ] [ i ] = t r u e f [ i ] [ i + 1 ] = t r u e i f S [ i ] = = S [ i + 1 ] \begin{cases} f[i][i]=true \\ f[i][i+1]=true \ if \ S[i]==S[i+1]\\ \end{cases} {f[i][i]=truef[i][i+1]=true if S[i]==S[i+1]
- f [ i ] [ i ] = t r u e f[i][i]=true f[i][i]=true 表示单个字符为回文
- f [ i ] [ i + 1 ] = t r u e i f S [ i ] = = S [ i + 1 ] f[i][i+1]=true \ if \ S[i]==S[i+1] f[i][i+1]=true if S[i]==S[i+1] 表示两个相连字符为回文
C++代码:
class Solution {
public:
string longestPalindrome(string s)
{
//如果字符串为空,那么返回空
if (s.empty()) return "";
//字符串长度
int len = s.size();
//单个字符
if (len == 1) return s;
//保存最长回文子串长度
int longest = 1;
//保存最长回文子串起点
int start=0;
//定义了一个vector容器,元素类型为vector<int>,初始化为包含len个vector<int>对象,
//每个对象都是一个新创立的vector<int>对象的拷贝,而这个新创立的vector<int>对象被初始化为包含len个0。
//类似于创建了一个lenxlen的二维数组,可以通过dp[i][j]的方式来访问元素
vector<vector<int>> dp(len,vector<int>(len));
//初始条件
for (int i = 0; i < len; i++)
{
//二维数组对角线为单个字符,都为1
dp[i][i] = 1;
if(i<len-1)
{
if (s[i] == s[i + 1])//两个相连字符相同
{
dp[i][i + 1] = 1;//为回文,标为1
start=i;//变更起始位置
longest=2;//最长回文子串长度为2
}
}
}
//字符串长度>=3
for (int l = 3; l <= len; l++)//遍历每种子串长度
{
for (int i = 0; i+l-1 < len; i++)//枚举子串的起始点
{
int j=l+i-1;//终点
if (s[i] == s[j] && dp[i+1][j-1]==1) //如果子串两端字符相同,并且去掉两端字符的子串是回文子串
{
dp[i][j] = 1;//此时该子串为回文,标为1
start=i;//更新开始位置
longest = l;//更新最长长度
}
}
}
return s.substr(start,longest);//截取字符串中有start开始,截取longest个字符
}
};
Java代码:
class Solution {
public String longestPalindrome(String s) {
if("".equals(s)){
return "";
}
int len = s.length();
if(len == 1){
return s;
}
int sLength = 1;
int start = 0;
int[][] dp = new int[len][len];
for(int i = 0; i < len; i++){
dp[i][i] = 1;
if(i < len - 1 && s.charAt(i) == s.charAt(i+1)){
dp[i][i+1] = 1;
sLength = 2;
start = i;
}
}
for(int l = 3; l <= len; l++){
for(int i = 0; i + l -1 < len; i++){
int j = i + l - 1;
if(s.charAt(i) == s.charAt(j) && dp[i+1][j-1] == 1){
dp[i][j] = 1;
start = i;
sLength = l;
}
}
}
return s.substring(start,start+sLength);
}
}
Python3代码:
class Solution:
def longestPalindrome(self, s: str) -> str:
length = len(s) # 计算字符串长度
if length == 0:
return ""
if length == 1:
return s
sLength = 1
start = 0
dp = [[0 for i in range(length)] for j in range(length)] # 创建二维数组,初始化为0
for i in range(0, length):
dp[i][i] = 1
if i < length - 1 and s[i] == s[i+1]:
dp[i][i+1] = 1
sLength = 2
start = i
for l in range(3, length+1):
for i in range(0, length-l+1):
j = i + l - 1
if s[i] == s[j] and dp[i+1][j-1] == 1:
dp[i][j] = 1
start = i
sLength = l
return s[start : start+sLength]
3、中心扩展法
中心扩展法的思想是,遍历到数组的某一个元素时,以这个元素为中心,向两边进行扩展,如果两边的元素相同则继续扩展,否则停止扩展。算法复杂度为 O ( N 2 ) O(N^2) O(N2)。
如下图:当遍历到3时
但是单个字符扩展存在缺陷,当字符串长度为偶数时,例如:1221
1,2,2,1是一个回文串,然而找不到对称中心,这样以一个元素为中心向两边扩展就不好用了
- 1、分别以单个字符和相邻两个字符为中心扩展(下面代码使用的是此方法)
- 2、对1,2,2,1进行填充,比如说用#进行填充得到:#,1,#,2,#,2,#,1,#
Java代码:
class Solution {
public String longestPalindrome(String s) {
if(s == null || s.length() < 1)
return "";
int start = 0;
int end = 0;
//中心扩展法,依次遍历中心点
for(int i = 0; i < s.length(); i++){
//求扩展中心的长度
int len1 = expandLen(s, i, i); //以每个字符为中心
int len2 = expandLen(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 expandLen(String s, int L, int R){
int left = L;
int right = R;
while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
left--;
right++;
}
return right - left - 1;
}
}
C++代码:
class Solution {
public:
string longestPalindrome(string s) {
int len=s.size();
if(len==0||len==1)
return s;
int start=0;//记录回文子串起始位置
int end=0;//记录回文子串终止位置
int mlen=0;//记录最大回文子串的长度
for(int i=0;i<len;i++)
{
int len1=expendaroundcenter(s,i,i);
int len2=expendaroundcenter(s,i,i+1);
mlen=max(max(len1,len2),mlen);
if(mlen>end-start+1)
{
start=i-(mlen-1)/2;
end=i+mlen/2;
}
}
return s.substr(start,mlen);
//该函数的意思是获取从start开始长度为mlen长度的字符串
}
private:
int expendaroundcenter(string s,int left,int right)
//计算以left和right为中心的回文串长度
{
int L=left;
int R=right;
while(L>=0 && R<s.length() && s[R]==s[L])
{
L--;
R++;
}
return R-L-1;
}
};
Python3代码:
class Solution:
def longestPalindrome(self, s: str) -> str:
def expand(s, left, right):
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return right - left - 1
start = 0
end = 0
for i in range(len(s)):
len1 = expand(s, i, i)
len2 = expand(s, i, i+1)
max_len = max(len1, len2)
if max_len > end - start:
start = i - int((max_len-1)/2)
end = i + int(max_len/2)
return s[start : end+1]
Manacher算法
直接通过例子来说明:
Manacher算法的核心思想,就是利用前面遍历的时候产生的回文子串
1、原理
如上图:
- i d x idx idx 表示为蓝色回文子串的对称轴(已知)
- 现在求以 c u r cur cur 为对称轴的回文子串(未知)
- p r e pre pre 为以 i d x idx idx 为对称轴, c u r cur cur 的对称位置(遍历到 c u r cur cur, p r e pre pre就已知了)
情况一: i ′ i' i′ 的回文子串超出 i d x idx idx 的回文子串的左边界
已知 i d x idx idx 为蓝色块子串的中心轴,现在求以i为中心轴的回文子串
- i i i 处于以 i d x idx idx 为中心轴的回文子串中, i ′ i' i′ 为 i i i 关于 i d x idx idx 的对称点,且以 i ′ i' i′ 为中心轴的回文子串已知(橘黄色块)
- 其中 i i i 指向 c c c, i ′ i' i′ 指向 b b b, i d x idx idx 指向 e e e(小写字符为变量,大写字母为具体字符)
- 那么由 e e e 为中心的回文子串中得知, b = c b=c b=c
- 又因为 i d x idx idx 的回文不包括 a a a 和 d d d,所以 a ! = d a !=d a!=d( a = d a=d a=d 时,以 i d x idx idx 的回文子串还要扩展下去)
- 又因为 i d x idx idx 左到 b b b 和 i d x idx idx右到 c c c 相等的,且 a ! = d a!=d a!=d,所以以 c c c 为中心轴的回文半径只有 i d x 右 − l o c a t i o n ( c ) idx右-location(c) idx右−location(c)
- 若 a a a 关于以 b b b 为中心的回文的对称点为 a ′ a' a′。 a ′ a' a′ 以 e e e 为中心轴的回文的对称点为 a ′ ′ a'' a′′,那么 a = a ′ = a ′ ′ ! = d a=a'=a''!=d a=a′=a′′!=d
举例说明:
由于存在字符串长度为偶数和奇数,我们使用#填充,如下:
- 当遍历到13号B时,以9号D为中心轴的回文子串从2号到16号(由于前面已经遍历过,已知),长度为 16 − 2 + 1 = 15 16-2+1=15 16−2+1=15
- 以5号B为中心轴的回文子串从0号到10号(已知),长度为 10 − 0 + 1 = 11 10-0+1=11 10−0+1=11
- 13号B关于9号D的对称点为5号B,现在要求以13号B为对称轴的回文子串
1号D和17号E不相等,现在只要盘判定以13号B为中心轴的回文子串是否包含17号
- 如果包括17号E,那么它关于13号B对称的点就是9号D,而9号D关于5号B的对称点就是1号D
- 根据对称性可知,17号E应该等于9号D等于1号D,很显然不相等
所以以13号B为中心轴的回文子串不包括17号E,又根据以5号B和9号D为中心轴的回文子串可知:
- 2号#到5号B等于8号#到5号B
- 10号#到13号B等于16号#到13号B
情况二: i ′ i' i′ 的回文子串 i d x idx idx 的回文子串包含
已知 i i i 关于 i d x idx idx 为中心轴的对称点 i ′ i' i′ 的最大回文子串如上图
- 因为 i ′ i' i′ 的回文子串不包括 a , b a,b a,b 则 a ! = b a!=b a!=b
- 又因为 a , d a,d a,d 和 b , c b,c b,c 分别关于 i d x idx idx对称,记 b = c , a = d b=c,a=d b=c,a=d ,所以 c ! = d c!=d c!=d
- 又因为在 c c c 和 d d d 之间是回文,原因在于 c c c 和 d d d 之间的字符关于 i d x idx idx 的 a a a 和 b b b 之间对称,且 a a a 和 b b b 之间是回文串,所以, c c c 和 d d d 之间也是回文串。所以 i i i 的回文子串的长度和 i ′ i' i′ 相同
举例说明:
情况三: i ′ i' i′ 的回文子串的左边界与 i d x idx idx 的回文子串的左边界重合
由 i d x idx idx 为中心轴的回文子串可知, b = c , a ! = d b=c,a!=d b=c,a!=d,且 i ′ i' i′ 的回文长度在 a a a 到 b b b 之间(不包括 a , b a,b a,b)
那么 i i i 的回文子串的长度至少如上图所示
- 若 c = d c=d c=d 时,关于 i i i 为中心轴的回文还是可以扩展的
- 若 c ! = d c!=d c!=d 则刚好是上图所示的。
举例说明(c=d时):
i i i 关于 i d x idx idx 的对称点 i ′ i' i′ 的最长回文子串如上图,且 i ′ i' i′ 的回文左边界与 i d x idx idx 重合,所以 i i i 为中心的回文需要从蓝色框边界开始在往左右两边试着扩展
情况四: i ′ i' i′ 的回文子串没有被 i d x idx idx 的回文子串包含
此时,我们没有任何信息可以利用,只能以 i i i 为中心轴,向左右两边扩展。找出它的最长回文子串。
2、C++
代码
class Solution {
public:
string longestPalindrome(string s) {
//计算字符串长度
int n = s.size();
if(n <= 1) return s;
//生成一个字符串str包含2*n+1个字符0
string str(2*n+1,'0');
bool flag = 1;
int j = 0;
int maxIdx = 0;
//填充字符,得到形如:#1#2#3#2#1# 的字符串
for(int i=0; i<2*n+1; i++){
if(flag){
str[i]='#';
flag = false;
}else{
str[i] = s[j++];
flag = true;
}
}
//表示以i为中心轴的回文子串的半径,不包含对称轴
//如abcdcba,d的下标为4,radius[4] = 3,radius[0] = 0
vector<int> radius(2*n+1,0);
int idx = 0; //表示上一次回文子串的中心轴下标
int rad = 1; //idx能够包含最大的范围的下一个字符下标
for(int i=1; i<2*n+1; i++){
//情况四
if(i >= rad){
forceExtend(str, radius, idx, rad, i);
maxIdx = (radius[i] > radius[maxIdx] ? i : maxIdx);
}else if(i < rad){
int j = 2*idx - i; //i关于idx的对称点j
int idx_radius = idx - radius[idx]; //idx回文子串的左边界下标
int j_radius = j - radius[j];//j的回文子串的左边界下标
if(j_radius > idx_radius){ //情况二
radius[i] = radius[j]; //i的回文子串和其关于idx对称点的回文子串长度一样
}else if(j_radius < idx_radius){//情况一
radius[i] = idx + radius[idx] - i;//idx的右边界下标-i下标
}else{ //情况三
radius[i] = idx + radius[idx] - i;//至少
int count = 1;
//相等时,继续扩展
while((i + radius[i] + count) <= str.size()
&& (i - radius[i] - count) >= 0
&& str[i + radius[i] + count] == str[i - radius[i] - count]){
count++;
}
//不等时
radius[i] += (count - 1);
//更新最长回文子串中心和右边界下一个字符下标
if(i + radius[i] >= rad){
idx = i;
rad = i + count;
}
}
//更新最长回文子串的中心
maxIdx = (radius[i] > radius[maxIdx] ? i : maxIdx);
}
}
string ret = getMaxSubString(str, maxIdx, radius[maxIdx]);
return ret;
}
//情况四
void forceExtend(const string& str, vector<int>& radius, int &idx, int &rad, const int k){
int count = 1;
while((k - count) >=0
&& (k + count) < str.size()
&& str[k - count] == str[k + count]){
count++;
}
radius[k] = count - 1;
if((k + radius[k]) >= rad){
idx = k;
rad = k + count;
}
}
//求出最长回文子串
string getMaxSubString(const string &str,const int k,const int r){
string ret(r, '0');
int j = 0;
for(int i = k-r+1; i <= k+r; i+=2){
ret[j++] = str[i];
}
return ret;
}
};
3、Java
代码
public class Solution {
public static String longestPalindrome(String s) {
int n = s.length();
if (n <= 1) return s ;
StringBuilder strb = new StringBuilder();
strb.append("#");
for (int i = 0; i < s.length(); i++) {
strb.append(s.charAt(i));
strb.append("#");
}
int len = strb.length();
int[] radius = new int[len];
int idx = 0; //表示上一次回文子串的中心轴下标
int rad = 1; //idx能够包含最大的范围的下一个字符下标
int j = 0;
int maxIdx = 0;
for (int i = 1; i < len; i++) {
//情况四
if(i >= rad){
int count = 1;
while((i - count) >=0
&& (i + count) < strb.length()
&& strb.charAt(i - count) == strb.charAt(i + count)){
count++;
}
radius[i] = count - 1;
if((i + radius[i]) >= rad){
idx = i;
rad = i + count;
}
maxIdx = (radius[i] > radius[maxIdx] ? i : maxIdx);
}else if(i < rad){
j = 2*idx - i; //i关于idx的对称点j
int idx_radius = idx - radius[idx]; //idx回文子串的左边界下标
int j_radius = j - radius[j];//j的回文子串的左边界下标
if(j_radius > idx_radius){ //情况二
radius[i] = radius[j]; //i的回文子串和其关于idx对称点的回文子串长度一样
}else if(j_radius < idx_radius){//情况一
radius[i] = idx + radius[idx] - i;//idx的右边界下标-i下标
}else{ //情况三
radius[i] = idx + radius[idx] - i;//至少
int count2 = 1;
//相等时,继续扩展
while((i + radius[i] + count2) < len
&& (i - radius[i] - count2) >= 0
&& strb.charAt(i + radius[i] + count2) == strb.charAt(i - radius[i] - count2)){
count2++;
}
//不等时
radius[i] += (count2 - 1);
//更新最长回文子串中心和右边界下一个字符下标
if(i + radius[i] >= rad){
idx = i;
rad = i + count2;
}
}
//更新最长回文子串的中心
maxIdx = (radius[i] > radius[maxIdx] ? i : maxIdx);
}
}
StringBuilder ret = new StringBuilder();
for(int i = maxIdx-radius[maxIdx]+1; i <= maxIdx + radius[maxIdx]; i+=2){
ret.append(strb.charAt(i));
}
return ret.toString();
}
}
4、Python3
代码
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n <= 1:
return s
st = '#' + '#'.join(s) + '#'
sLen = len(st)
radius = [0] * sLen
idx = 0
rad = 1
maxIdx = 0
for i in range(1, sLen):
if i >= rad:
count = 1
while (i - count) >= 0 and (i + count) < sLen and st[i - count] == st[i + count]:
count += 1
radius[i] = count - 1
if (i + radius[i]) >= rad:
idx = i
rad = i + count
maxIdx = i if (radius[i] > radius[maxIdx]) else maxIdx
elif i < rad:
j = 2 * idx - i
idx_radius = idx - radius[idx]
j_radius = j - radius[j]
if j_radius > idx_radius:
radius[i] = radius[j]
elif j_radius < idx_radius:
radius[i] = idx + radius[idx] - i
else:
radius[i] = idx + radius[idx] - i
count2 = 1
while (i + radius[i] + count2) < sLen and (i - radius[i] - count2) >= 0 and st[i + radius[i] + count2] == st[i - radius[i] - count2]:
count2 += 1
radius[i] += (count2 - 1)
if (i + radius[i]) >= rad:
idx = i
rad = i + count2
maxIdx = i if (radius[i] > radius[maxIdx]) else maxIdx
ret = []
for i in range(maxIdx-radius[maxIdx]+1, maxIdx + radius[maxIdx]+1, 2):
ret.append(st[i])
return ''.join(ret)