题目描述
给定一个字符串 s
,找到 s
中最长的回文子串。你可以假设 s
的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
暴力法
扫描一遍字符串,判断 范围内的字符串是不是回文字符串,并且在这个过程中记录最长的字符串
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() 2) {
return s;
}
char[] str = s.toCharArray();
int start = 0;
int maxLength = 1;
for (int i = 0; i 1; i++) {
for (int j = i + 1; j // 如果 [i,j] 范围的字符串长度小于目前已知的最长回文字符串的长度,则无需判断
if ((j - i + 1) > maxLength) {
// 如果是回文字符串,更新最长回文字符串的长度,以及该回文字符串的起始位置
if (isPalindrome(str, i, j)) {
maxLength = j - i + 1;
start = i;
}
}
}
}
return s.substring(start, start + maxLength);
}
// 判断 [start, end] 范围内的字符串是否为回文字符串
private boolean isPalindrome(char[] str, int start, int end) {
while (start <= end) {
// 判断两端的字符串是否相同,不相同则不可能是回文字符串
if (str[start] != str[end]) {
return false;
}
start++;
end--;
}
return true;
}
}
动态规划
我们开辟一个数组 , 表示 范围内的字符串是否为回文字符串,那我们可以知道
当 的时候,那么不可能是回文字符串,如果 的时候,如果 是回文字符串,那么 就是回文字符串。
但是如果 的话,这时 没有意义,其实这种情况就是 内只有两个字符,如果 的话,那么它就是一个回文字符串,所以这时可以直接得到 。
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() 2) {
return s;
}
int length = s.length();
boolean dp[][] = new boolean[length][length];
for (int i = 0; i for (int j = 0; j dp[i][j] = false;
}
}
// [i,i] 表示单个字符,肯定是回文字符串
for (int i = 0; i dp[i][i] = true;
}
int maxLength = 1;
int begin = 0;
// 因为 [i, j] 要用到 [i + 1, j - 1],所以让 i 动
// 如果让 j 动,那么 [i + 1, j -1] 的范围没都判断过,会导致错误的结果
for (int j = 1; j for (int i = 0; i // 如果字符两端不相等,不是回文字符串
if (s.charAt(i) != s.charAt(j)) {
dp[i][j] = false;
} else {
// 如果只有两个元素,可以直接判断为回文字符串
if (i + 1 > j - 1) {
dp[i][j] = true;
} else {
// 与 dp[i + 1][j - 1] 相同
dp[i][j] = dp[i + 1][j - 1];
}
}
// 更新最长长度以及起始位置
if (dp[i][j] && (j - i + 1) > maxLength) {
maxLength = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLength);
}
}
中间扩散法
上面的情况都是检查某个范围内的字符串是否是回文字符串,而中心扩散法,则是以某点为中心进行向外扩散,在扩散的同时检查是否是回文字符串,过程如下
- 检查该点左右两边的字符串是否相等,如果相等则继续扩散,如果不相等则停止扩散
- 停止扩散后记录该回文字符串的长度,如果大于当前记录的最大回文字符串的长度,则进行更新,为了获得该回文字符串,还需要记录该回文字符串的起始位置
需要注意的是,既可以从某个字符开始扩散,也可以从字符间的空隙进行扩散
从某个点进行扩散的代码如下:
private int centerSpread(String s, int left, int right) {
int length = 0;
while(left >=0 && right if (!(s.charAt(left) == s.charAt(right))) {
break;
}
length++;
left--;
right++;
}
return length;
}
如果从某个字符进行扩散,那么 left 和 right 都是该字符的下标,如果从某个字符空隙进行扩散,则 left 和 right 分别为该空隙左右两边字符的下标。扩散的示意图如下
完整代码如下
class Solution {
public String longestPalindrome(String s) {
int begin = 0;
int maxLength = 1;
for (int i = 1; i // 从字符空隙进行扩散
int length1 = centerSpread(s, i - 1, i);
// 从字符进行扩散
int length2 = centerSpread(s, i, i);
// 获得二者进行扩散的回文字符串的最大长度
int length = (2 * length1) >= (2 * length2 - 1) ? 2 * length1 : (2 * length2 - 1);
// 如果比 maxLength 大,则进行更新
if (length > maxLength) {
maxLength = length;
begin = i - length/2;
}
}
return s.substring(begin, begin + maxLength);
}
private int centerSpread(String s, int left, int right) {
int length = 0;
while(left >=0 && right if (!(s.charAt(left) == s.charAt(right))) {
break;
}
length++;
left--;
right++;
}
return length;
}
}
Manacher算法
Manacher 算法是对中间扩散法的改进,首先我们在字符间填充 #
以此充当字符空隙,得到一个新的字符串,然后用一个数组记录新字符串每个字符能够扩散多远
b | # | a | # | b | # | a | # | d |
---|---|---|---|---|---|---|---|---|
我们复用中心扩散法的代码
class Solution {
public String longestPalindrome(String s) {
int begin = 0;
int maxLength = 1;
// babad => b#a#b#a#d
String str = String.join( "#", s.split(""));
for (int i = 1; i int length = centerSpread(str, i, i);
length = 2 * length - 1;
if (length >= maxLength) {
// #a# b#b 二者长度相同,但是应选择后面的
if (length > maxLength || str.charAt(i + length/2) != '#') {
maxLength = length;
begin = i - length/2;
}
}
}
// b#a#b => bab
return String.join( "", str.substring(begin, begin + maxLength).split("#"));
}
private int centerSpread(String s, int left, int right) {
int length = 0;
while (left >= 0 && right if (!(s.charAt(left) == s.charAt(right))) {
break;
}
length++;
left--;
right++;
}
return length;
}
}
这个时候我们还没有使用上面提到的数组,接下来我们需要利用数组记录一些信息,从而加快中间扩散法的速度。
首先我们要知道如何加快中间扩散法的速度,这就需要利用回文字符串的特点,那就是回文字符串是左右对称的,当我们对回文字符串内的某个字符进行中间扩散时,我们可以先找到对称位置的字符,看看它能够扩散的多远
如果它扩散的距离小于所处回文字符串的范围,那么根据对称的特点,我们不需要对该字符进行中间扩散,它能够扩散的距离就是对称位置所能扩散的距离
但是对称位置能够扩散的距离刚好在所处回文字符串的边界,或者大于所处的回文字符串,那么此时扩散的距离就不等于对称位置能够扩散的距离,而是需要进行扩散才能确定
但是我们不需要从该字符串开始进行扩散,因为我们可以确定在回文字符串的范围内,它是对称的,所以我们只需要从回文字符串的外部进行扩散即可
扩散完毕后记录能够扩散后的距离,并决定是否更新 begin,maxLength。
现在还有一个问题,如何知道某个字符处于一个回文字符串中,我们通过记录两个变量 center 和 maxRight 来表示一个回文字符串,center 表示回文字符串的中心,maxRight 表示中心离回文字符串右边界的距离
我们认为如果字符的下标小于 ,那么它就在一个回文字符串中。
那什么时候更新这两个变量呢? 那就是在某个字符扩散之后,他向右边能够扩散的范围大于当前的 ,那么就更新 center 和 maxRight。
代码如下:
class Solution {
public String longestPalindrome(String s) {
// 将字符串转化为 a#b#c#b 的形式
// String newStr = String.join( "#", s.split(""));
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i if (i != s.length() - 1) {
stringBuilder.append(s.charAt(i) + "#");
} else {
stringBuilder.append(s.charAt(i));
}
}
String newStr = stringBuilder.toString();
// records[i] 表示扩散距离
int[] records = new int[newStr.length()];
records[0] = 0;
// 回文字符串的起始位置和长度
int begin = 0;
int maxLength = 1;
// 表示回文字符串
int center = 0;
int maxRight = 0;
for (int i = 1; i // 如果 str[i] 在某个回文字符串中
if (i <= center + maxRight) {
// 对称位置的扩散距离
int mirrorLength = records[2 * center - i];
// 超过或等于 center + maxRight 表示的边界,进行中间扩散,并进行必要的更新
if (i + mirrorLength >= center + maxRight) {
// 扩散
int length = center + maxRight - i;
while (i + length + 1 1) >= 0) {
if (newStr.charAt(i + length + 1) != newStr.charAt(i - length - 1)) {
break;
}
length++;
}
records[i] = length;
// 更新
if (2 * length + 1 >= maxLength) {
if (2 * length + 1 > maxLength || newStr.charAt(center) != '#') {
begin = i - length;
maxLength = 2 * length + 1;
}
}
if (i + length > center + maxRight) {
center = i;
maxRight = length;
}
} else {
// 小于 center + maxRight 形成的边界,直接使用对称位置的值
records[i] = mirrorLength;
}
} else {
// 不在回文字符串中,进行中心扩散,并进行必要的更新
int length = 0;
while (i + length + 1 1) >= 0) {
if (newStr.charAt(i + length + 1) != newStr.charAt(i - length - 1)) {
break;
}
length++;
}
if (2 * length + 1 > maxLength) {
begin = i - length;
maxLength = 2 * length + 1;
}
center = i;
maxRight = length;
records[i] = length;
}
}
// 将 a#b#b#a 形式的字符串转化为 abba
// return String.join( "", newStr.substring(begin, begin + maxLength).split("#"));
String ss = newStr.substring(begin, begin + maxLength);
StringBuilder stringBuilder1 = new StringBuilder();
for (int i = 0; i if (ss.charAt(i) != '#') {
stringBuilder1.append(ss.charAt(i));
}
}
return stringBuilder1.toString();
}
}