给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba"也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
对于回文子串,会有这么一个性质,假设字符串s的s[i]-s[j] (j>i)为回文子串,那么s[i+1]-s[j-1]也必定是回文串。那么根据这个性质我们就能将问题分解为更小的部分,如果判断s[i-j]是不是回文子串,只需要判断s[i]==s[j] 以及s[i-j]是不是回文子串。如果我们从长度为1和2的子串开始判断,然后后面更长的子串就可以依据前面的计算结果更快的判断出来。
时间复杂度:O(n^2)
空间复杂度:O(n^2)
实现如下:
/**
* dp解法:
* dpMap[][]用于存储回文串信息 dp[i][j]表示 s[j]到s[i]为回文串
* 初始化: dp[i][i] = true 如果s[i]==s[i+1] 则dpMap[i+1][i]=true
* 规则:如果s[i] == s[j] && dpMap[i-1][j+1]=true 则 dpMap[i][j] = true
*
* @param s 字符串
* @return 最长回文串
*/
public String longestPalindrome(String s) {
//存储回文串 dpMap[i][j]=true 表示 s[j]到s[i]为回文串
boolean dpMap[][] = new boolean[s.length()][s.length()];
//存取最长回文串起点
int start = 0;
//存取最长回文串长度
int maxLength = 1;
//初始化dpMap 处理里单个字符和两个字符的情况
for (int i = 0; i < s.length(); i++) {
dpMap[i][i] = true;
if (i + 1 < s.length() && s.charAt(i) == s.charAt(i + 1)) {
dpMap[i + 1][i] = true;
start = i;
maxLength = 2;
}
}
// dpMap[i-1][j+1] == true && s[i] == s[j] ==> dpMap[i][j] = true
for (int i = 2; i < s.length(); i++) {
for (int j = 0; j < i - 1; j++) {
if (dpMap[i - 1][j + 1] && s.charAt(i) == s.charAt(j)) {
dpMap[i][j] = true;
if (i - j + 1 > maxLength) {
maxLength = i - j + 1;
start = j;
}
}
}
}
return s.substring(start, start + maxLength);
}
选定字符串的一个字符,依次向两边逐渐拓展,每次位移长度为1,如果两边选取的字符相同,那么是回文串,继续拓展,否则终止,选取下一个字符。需要注意的是分aba、abba两种形式的情况。
时间复杂度:O(n^2)
空间复杂度:O(1)
这个解法比上个速度更慢,但是空间复杂度较低
解法如下:
private String getChildStr(String s, int pre, int aft) {
int len = s.length();
while (pre >= 0 && aft < len && s.charAt(pre) == s.charAt(aft)) {
pre--;
aft++;
}
return s.substring(pre + 1, aft);
}
/**
* 中心拓展法:
* 选定一个字符,往两边拓展更新最长子串
* 需要注意 aba abba这两种形式的情况
*
* @param s 字符串
* @return 最长回文串
*/
public String longestPalindrome1(String s) {
int len = s.length();
if (len < 2) {
return s;
}
String rs = "";
for (int i = 1; i < len; i++) {
//判断aba情况
String temp = getChildStr(s, i, i);
if (temp.length() > rs.length()) {
rs = temp;
}
//判断abba情况
temp = getChildStr(s, i - 1, i);
if (temp.length() > rs.length()) {
rs = temp;
}
}
return rs;
}
具体算法原理参考:https://segmentfault.com/a/1190000008484167
上面博客在讲述为何p[i] = min(p[2 * id - i], mx - i)时候讲的不是那么容易理解。
对于第一种情况:
如果a部分超出mx,根据回文串性质,则a=b。此时根据回文串性质又可以推出b=c,若p[i]取值能超出mx的话,根据回文串性质c=d,此时可以接着推导出a=d,那么这时候p[id]的回文串长度为mx就不成立了。所以不存在这种情况。
对于第二种情况:
j完全在内部的时候,如果取值超过j的回文串长度,那么根据回文串性质,与第一种情况分析相似,p[j]的长度就不成立,所以也不存在。
对于第三种情况:
刚好是mx的情况,从mx再向外拓展不会受之前结果的影响。
实现如下:
/**
* Manacher算法
* @param s 字符串
* @return 最长回文子串
*/
public String longestPalindrome2(String s) {
//字符串预处理 aaa ==> $a#a#a#
StringBuilder tempStr = new StringBuilder("$");
for (int i = 0; i < s.length(); ++i) {
tempStr.append(s.charAt(i)).append("#");
}
tempStr.append("\0");
s = tempStr.toString();
//用StringBuilder比较好 这里偷懒写法
// s = Arrays.stream(s.split("")).reduce("$", (pre, next)->pre+next+"#") + "\0";
int[] dp = new int[s.length()];
int maxLength = 1;
int start = 1;
int id = 0;
int mx = 0;
int sLen = s.length() - 1;
for (int i = 1; i < sLen; ++i) {
//如果在mx(在回文串内或者刚好在边界)范围内 取dp[i] = Math.min(dp[2 * id - i], mx - i) 否则dp[i]=1
if (i < mx) {
dp[i] = Math.min(dp[2 * id - i], mx - i);
} else {
dp[i] = 1;
}
//刚好在边界 或者边界以外 需要往外扩张
while (s.charAt(i - dp[i]) == s.charAt(i + dp[i])) {
dp[i]++;
}
//如果mx最大值右移了 更新mx和id
if (mx < i + dp[i]) {
id = i;
mx = i + dp[i];
}
//更新最长回文串
if (maxLength < dp[i]) {
maxLength = dp[i];
start = i;
}
}
return s.substring(start - maxLength + 1, start + maxLength).replaceAll("$|#", "");
}