leetcode-master/problems/0005.最长回文子串.md

685 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<a href="https://programmercarl.com/other/xunlianying.html" target="_blank">
<img src="../pics/训练营.png" width="1000"/>
</a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 5.最长回文子串
[力扣题目链接](https://leetcode.cn/problems/longest-palindromic-substring/)
给你一个字符串 s找到 s 中最长的回文子串。
示例 1
* 输入s = "babad"
* 输出:"bab"
* 解释:"aba" 同样是符合题意的答案。
示例 2
* 输入s = "cbbd"
* 输出:"bb"
示例 3
* 输入s = "a"
* 输出:"a"
示例 4
* 输入s = "ac"
* 输出:"a"
## 思路
本题和[647.回文子串](https://programmercarl.com/0647.回文子串.html) 差不多是一样的但647.回文子串更基本一点建议可以先做647.回文子串
### 暴力解法
两层for循环遍历区间起始位置和终止位置然后判断这个区间是不是回文。
时间复杂度O(n^3)
### 动态规划
动规五部曲:
1. 确定dp数组dp table以及下标的含义
布尔类型的dp[i][j]:表示区间范围[i,j] 注意是左闭右闭的子串是否是回文子串如果是dp[i][j]为true否则为false。
2. 确定递推公式
在确定递推公式时,就要分析如下几种情况。
整体上是两种就是s[i]与s[j]相等s[i]与s[j]不相等这两种。
当s[i]与s[j]不相等那没啥好说的了dp[i][j]一定是false。
当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
* 情况一下标i 与 j相同同一个字符例如a当然是回文子串
* 情况二下标i 与 j相差为1例如aa也是文子串
* 情况三下标i 与 j相差大于1的时候例如cabac此时s[i]与s[j]已经相同了我们看i到j区间是不是回文子串就看aba是不是回文就可以了那么aba的区间就是 i+1 与 j-1区间这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
以上三种情况分析完了,那么递归公式如下:
```CPP
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
dp[i][j] = true;
}
}
```
注意这里我没有列出当s[i]与s[j]不相等的时候因为在下面dp[i][j]初始化的时候就初始为false。
在得到[i,j]区间是否是回文子串的时候,直接保存最长回文子串的左边界和右边界,代码如下:
```CPP
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
dp[i][j] = true;
}
}
if (dp[i][j] && j - i + 1 > maxlenth) {
maxlenth = j - i + 1;
left = i;
right = j;
}
```
3. dp数组如何初始化
dp[i][j]可以初始化为true么 当然不行,怎能刚开始就全都匹配上了。
所以dp[i][j]初始化为false。
4. 确定遍历顺序
遍历顺序可有有点讲究了。
首先从递推公式中可以看出情况三是根据dp[i + 1][j - 1]是否为true在对dp[i][j]进行赋值true的。
dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图:
![647.回文子串](https://code-thinking-1253855093.file.myqcloud.com/pics/20210121171032473.jpg)
如果这矩阵是从上到下从左到右遍历那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。
**所以一定要从下到上从左到右遍历这样保证dp[i + 1][j - 1]都是经过计算的**
有的代码实现是优先遍历列然后遍历行其实也是一个道理都是为了保证dp[i + 1][j - 1]都是经过计算的。
代码如下:
```CPP
for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
dp[i][j] = true;
}
}
if (dp[i][j] && j - i + 1 > maxlenth) {
maxlenth = j - i + 1;
left = i;
right = j;
}
}
}
```
5. 举例推导dp数组
举例,输入:"aaa"dp[i][j]状态如下:
![647.回文子串1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210121171059951.jpg)
**注意因为dp[i][j]的定义所以j一定是大于等于i的那么在填充dp[i][j]的时候一定是只填充右上半部分**
以上分析完毕C++代码如下:
```CPP
class Solution {
public:
string longestPalindrome(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
int maxlenth = 0;
int left = 0;
int right = 0;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
dp[i][j] = true;
}
}
if (dp[i][j] && j - i + 1 > maxlenth) {
maxlenth = j - i + 1;
left = i;
right = j;
}
}
}
return s.substr(left, right - left + 1);
}
};
```
以上代码是为了凸显情况一二三,当然是可以简洁一下的,如下:
```CPP
class Solution {
public:
string longestPalindrome(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
int maxlenth = 0;
int left = 0;
int right = 0;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
dp[i][j] = true;
}
if (dp[i][j] && j - i + 1 > maxlenth) {
maxlenth = j - i + 1;
left = i;
right = j;
}
}
}
return s.substr(left, maxlenth);
}
};
```
* 时间复杂度O(n^2)
* 空间复杂度O(n^2)
### 双指针
动态规划的空间复杂度是偏高的,我们再看一下双指针法。
首先确定回文串,就是找中心然后想两边扩散看是不是对称的就可以了。
**在遍历中心点的时候,要注意中心点有两种情况**
一个元素可以作为中心点,两个元素也可以作为中心点。
那么有的同学问了,三个元素还可以做中心点呢。其实三个元素就可以由一个元素左右添加元素得到,四个元素则可以由两个元素左右添加元素得到。
所以我们在计算的时候,要注意一个元素为中心点和两个元素为中心点的情况。
**这两种情况可以放在一起计算,但分别计算思路更清晰,我倾向于分别计算**,代码如下:
```CPP
class Solution {
public:
int left = 0;
int right = 0;
int maxLength = 0;
string longestPalindrome(string s) {
int result = 0;
for (int i = 0; i < s.size(); i++) {
extend(s, i, i, s.size()); // 以i为中心
extend(s, i, i + 1, s.size()); // 以i和i+1为中心
}
return s.substr(left, maxLength);
}
void extend(const string& s, int i, int j, int n) {
while (i >= 0 && j < n && s[i] == s[j]) {
if (j - i + 1 > maxLength) {
left = i;
right = j;
maxLength = j - i + 1;
}
i--;
j++;
}
}
};
```
* 时间复杂度O(n^2)
* 空间复杂度O(1)
## 其他语言版本
### Java
```java
// 双指针 动态规划
class Solution {
public String longestPalindrome(String s) {
if (s.length() == 0 || s.length() == 1) return s;
int length = 1;
int index = 0;
boolean[][] palindrome = new boolean[s.length()][s.length()];
for (int i = 0; i < s.length(); i++) {
palindrome[i][i] = true;
}
for (int L = 2; L <= s.length(); L++) {
for (int i = 0; i < s.length(); i++) {
int j = i + L - 1;
if (j >= s.length()) break;
if (s.charAt(i) != s.charAt(j)) {
palindrome[i][j] = false;
} else {
if (j - i < 3) {
palindrome[i][j] = true;
} else {
palindrome[i][j] = palindrome[i + 1][j - 1];
}
}
if (palindrome[i][j] && j - i + 1 > length) {
length = j - i + 1;
index = i;
}
}
}
return s.substring(index, index + length);
}
}
```
```java
// 双指针 中心扩散法
class Solution {
public String longestPalindrome(String s) {
String s1 = "";
String s2 = "";
String res = "";
for (int i = 0; i < s.length(); i++) {
// 分两种情况:即一个元素作为中心点,两个元素作为中心点
s1 = extend(s, i, i); // 情况1
res = s1.length() > res.length() ? s1 : res;
s2 = extend(s, i, i + 1); // 情况2
res = s2.length() > res.length() ? s2 : res;
}
return res; // 返回最长的
}
public String extend(String s, int start, int end){
String tmp = "";
while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)){
tmp = s.substring(start, end + 1); // Java中substring是左闭右开的所以要+1
// 向两边扩散
start--;
end++;
}
return tmp;
}
}
```
### Python
```python
class Solution:
def longestPalindrome(self, s: str) -> str:
dp = [[False] * len(s) for _ in range(len(s))]
maxlenth = 0
left = 0
right = 0
for i in range(len(s) - 1, -1, -1):
for j in range(i, len(s)):
if s[j] == s[i]:
if j - i <= 1 or dp[i + 1][j - 1]:
dp[i][j] = True
if dp[i][j] and j - i + 1 > maxlenth:
maxlenth = j - i + 1
left = i
right = j
return s[left:right + 1]
```
双指针:
```python
class Solution:
def longestPalindrome(self, s: str) -> str:
def find_point(i, j, s):
while i >= 0 and j < len(s) and s[i] == s[j]:
i -= 1
j += 1
return i + 1, j
def compare(start, end, left, right):
if right - left > end - start:
return left, right
else:
return start, end
start = 0
end = 0
for i in range(len(s)):
left, right = find_point(i, i, s)
start, end = compare(start, end, left, right)
left, right = find_point(i, i + 1, s)
start, end = compare(start, end, left, right)
return s[start:end]
```
### Go
```go
func longestPalindrome(s string) string {
maxLen := 0
left := 0
length := 0
dp := make([][]bool, len(s))
for i := 0; i < len(s); i++ {
dp[i] = make([]bool,len(s))
}
for i := len(s)-1; i >= 0; i-- {
for j := i; j < len(s); j++ {
if s[i] == s[j]{
if j-i <= 1{ // 情况一和情况二
length = j-i
dp[i][j]=true
}else if dp[i+1][j-1]{ // 情况三
length = j-i
dp[i][j] = true
}
}
}
if length > maxLen {
maxLen = length
left = i
}
}
return s[left: left+maxLen+1]
}
```
### JavaScript
```js
//动态规划解法
var longestPalindrome = function(s) {
const len = s.length;
// 布尔类型的dp[i][j]:表示区间范围[i,j] 注意是左闭右闭的子串是否是回文子串如果是dp[i][j]为true否则为false
let dp = new Array(len).fill(false).map(() => new Array(len).fill(false));
// left起始位置 maxlenth回文串长度
let left = 0, maxlenth = 0;
for(let i = len - 1; i >= 0; i--){
for(let j = i; j < len; j++){
// 情况一下标i 与 j相同同一个字符例如a当然是回文子串 j - i == 0
// 情况二下标i 与 j相差为1例如aa也是文子串 j - i == 1
// 情况一和情况二 可以合并为 j - i <= 1
// 情况三下标i 与 j相差大于1的时候例如cabac此时s[i]与s[j]已经相同了我们看i到j区间是不是回文子串就看aba是不是回文就可以了那么aba的区间就是 i+1 与 j-1区间这个区间是不是回文就看dp[i + 1][j - 1]===true
if(s[i] === s[j] && (j - i <= 1 || dp[i + 1][j - 1])){
dp[i][j] = true;
}
// 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此时记录回文长度和起始位置
if(dp[i][j] && j - i + 1 > maxlenth) {
maxlenth = j - i + 1; // 回文串长度
left = i; // 起始位置
}
}
}
return s.substr(left, maxlenth); // 找到子串
};
//双指针
var longestPalindrome = function(s) {
let left = 0, right = 0, maxLength = 0;
const extend = (s, i, j, n) => {// s为字符串 i,j为双指针 n为字符串长度
while(i >= 0 && j < n && s[i] === s[j]){
if(j - i + 1 > maxLength){
left = i; // 更新开始位置
right = j; // 更新结尾位置
maxLength = j - i + 1; // 更新子串最大长度
}
// 指针移动
i--;
j++;
}
}
for(let i = 0; i < s.length; i++){
extend(s, i, i, s.length); // 以i为中心
extend(s, i, i + 1, s.length); // 以i和i+1为中心
}
return s.substr(left, maxLength);
};
//Manacher算法
var longestPalindrome = function(s) {
const len = s.length;
if(len < 2) return s;
let maxLength = 1, index = 0;
//Manacher算法,利用回文对称的性质根据i在上一个回文中心的臂长里的位置去判断i的回文性
//需要知道上一个回文中心,以及其臂长
let center = 0;
//注意这里使用了maxRight的而不是真实的臂长length,因为之后需要判断i在臂长的什么位置
//如果这里臂长用了length,之后还要 计算i - center 去和 length比较太繁琐
let maxRight = 0;
//考虑到回文串的长度是偶数的情况,所以这里预处理一下字符串,每个字符间插入特殊字符,把可能性都化为奇数
//这个处理把回文串长度的可能性都化为了奇数
//#c#b#b#a#
//#c#b#a#b#d#
let ss = "";
for(let i = 0; i < s.length; i++){
ss += "#"+s[i];
}
ss += "#";
//需要维护一个每个位置臂长的信息数组positionLength
const pl = new Array(ss.length).fill(0);
//这里需要注意参考的是i关于center对称的点i'的回文性
//i' = 2*center - i;
//所以列下情况:
//1.i>maxRight,找不到i',无法参考,自己算自己的
//2.i<=maxRight:
//2.1 i<maxRight-pl[i'],pl[i']的臂长没有超过center的臂长,根据对称性,pl[i] = pl[i']
//2.2 i=maxRight-pl[i'],pl[i']的臂长刚好等于center的臂长,根据对称性,pl[i] >= pl[i],大多少需要尝试扩散
//2.3 i>maxRight-pl[i'],pl[i']的臂长超过了center的臂长,根据对称性,i中心扩散到MaxRight处,
// s[2*i-maxRight] !== s[MaxRight]必不相等所以pl[i] = maxRight-i;
//总结就是pl[i] = Math.min(maxRight-i,pl[i']);提示i<maxRight-pl[i'] 也可写成 pl[i']<maxRight-i
//0没有意义,从1开始计算
for(let i = 1; i < ss.length; i++){
if(i <= maxRight){//可以参考之前的
pl[i] = Math.min(maxRight - i, pl[2 * center - i]);
//尝试中心扩散
}
//注意到i<maxRight时都要尝试中心扩散,所以写else完全无意义,把中心扩散的代码写在下面
// else{//i不在之前回文中心的臂长范围里,之前的信息就完全无法参考,只能从i中心扩散把然后去维护maxRight和center的定义
//尝试中心扩散
//这里不要动center和maxRight
// center = i;
// maxRight = pl[i] + i + 1;
let right = pl[i] + i + 1;
let left = i - pl[i] - 1;
while (left >= 0 && right<ss.length && ss[left] === ss[right]) {
right++;
left--;
pl[i]++;
}
// }
if(pl[i] + i > maxRight){
center = i;
maxRight = pl[i] + i;
}
if (pl[i] * 2 + 1 > maxLength){
maxLength = pl[i]*2+1;
index = i - pl[i];
}
}
return ss.substr(index, maxLength).replace(/#/g,"");
};
```
### C
动态规划:
```c
//初始化dp数组全部初始为false
bool **initDP(int strLen) {
bool **dp = (bool **)malloc(sizeof(bool *) * strLen);
int i, j;
for(i = 0; i < strLen; ++i) {
dp[i] = (bool *)malloc(sizeof(bool) * strLen);
for(j = 0; j < strLen; ++j)
dp[i][j] = false;
}
return dp;
}
char * longestPalindrome(char * s){
//求出字符串长度
int strLen = strlen(s);
//初始化dp数组元素初始化为false
bool **dp = initDP(strLen);
int maxLength = 0, left = 0, right = 0;
//从下到上,从左到右遍历
int i, j;
for(i = strLen - 1; i >= 0; --i) {
for(j = i; j < strLen; ++j) {
//若当前i与j所指字符一样
if(s[i] == s[j]) {
//若i、j指向相邻字符或同一字符则为回文字符串
if(j - i <= 1)
dp[i][j] = true;
//若i+1与j-1所指字符串为回文字符串则i、j所指字符串为回文字符串
else if(dp[i + 1][j - 1])
dp[i][j] = true;
}
//若新的字符串的长度大于之前的最大长度,进行更新
if(dp[i][j] && j - i + 1 > maxLength) {
maxLength = j - i + 1;
left = i;
right = j;
}
}
}
//复制回文字符串,并返回
char *ret = (char*)malloc(sizeof(char) * (maxLength + 1));
memcpy(ret, s + left, maxLength);
ret[maxLength] = 0;
return ret;
}
```
双指针:
```c
int left, maxLength;
void extend(char *str, int i, int j, int size) {
while(i >= 0 && j < size && str[i] == str[j]) {
//若当前子字符串长度大于最长的字符串长度,进行更新
if(j - i + 1 > maxLength) {
maxLength = j - i + 1;
left = i;
}
//左指针左移,右指针右移。扩大搜索范围
++j, --i;
}
}
char * longestPalindrome(char * s){
left = right = maxLength = 0;
int size = strlen(s);
int i;
for(i = 0; i < size; ++i) {
//长度为单数的子字符串
extend(s, i, i, size);
//长度为双数的子字符串
extend(s, i, i + 1, size);
}
//复制子字符串
char *subStr = (char *)malloc(sizeof(char) * (maxLength + 1));
memcpy(subStr, s + left, maxLength);
subStr[maxLength] = 0;
return subStr;
}
```
### C#
動態規則:
```c#
public class Solution {
public string LongestPalindrome(string s) {
bool[,] dp = new bool[s.Length, s.Length];
int maxlenth = 0;
int left = 0;
int right = 0;
for(int i = s.Length-1 ; i>=0; i--){
for(int j = i; j <s.Length;j++){
if(s[i] == s[j]){
if(j - i <= 1){ // 情况一和情况二
dp[i, j] = true;
}else if( dp[i+1, j-1] ){ // 情况三
dp[i, j] = true;
}
}
if(dp[i, j] && j-i+1 > maxlenth){
maxlenth = j-i+1;
left = i;
right = j;
}
}
}
return s.Substring(left, maxlenth);
}
}
```
雙指針:
```C#
public class Solution {
int maxlenth = 0;
int left = 0;
int right = 0;
public string LongestPalindrome(string s) {
int result = 0;
for (int i = 0; i < s.Length; i++) {
extend(s, i, i, s.Length); // 以i為中心
extend(s, i, i + 1, s.Length); // 以i和i+1為中心
}
return s.Substring(left, maxlenth);
}
private void extend(string s, int i, int j, int n) {
while (i >= 0 && j < n && s[i] == s[j]) {
if (j - i + 1 > maxlenth) {
left = i;
right = j;
maxlenth = j - i + 1;
}
i--;
j++;
}
}
}
```
<p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="../pics/网站星球宣传海报.jpg" width="1000"/>
</a>