最长回文子串
原文链接:https://www.leahy.club/archives/%E7%AE%97%E6%B3%95%E4%B8%8E%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2
1. 题目
2. 解答
常见的解法主要有:
暴力法:列举出所有可能的子串,并对每个子串进行分析。
最长公共子串法:根据回文的定义,正反读是一样的。那么可以将原理来的字符串倒置,从而将问题转化成了求解最长公共子串的问题。求解最长公共子串可以使用动态规划。
动态规划的状态转移方程:
a r r [ i ] [ j ] = a r r [ i − 1 ] [ j − 1 ] + 1 arr[i][j]=arr[i-1][j-1]+1 arr[i][j]=arr[i−1][j−1]+1
a r r [ i ] [ j ] arr[i][j] arr[i][j]保存的是公共子串的长度。
暴力法的优化:暴力法中存在很多重复判断,这里采用空间换时间的做法,存储中间结果换取低时间复杂度。
首先定义 p ( i , j ) p(i,j) p(i,j),
p ( i , j ) = { t r u e s[i,j]是回文串 f a l s e s[i,j]不是回文串 p(i,j)= \begin{cases} true& \text{s[i,j]是回文串}\\ false& \text{s[i,j]不是回文串} \end{cases} p(i,j)={truefalses[i,j]是回文串s[i,j]不是回文串
接下来有: p ( i , j ) = p ( i + 1 , j − 1 ) & & ( s [ i ] = = s [ j ] ) p(i,j)=p(i+1,j-1) \&\& (s[i]==s[j]) p(i,j)=p(i+1,j−1)&&(s[i]==s[j])
中心扩展算法:
回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。
Manacher算法(马拉车)算法:
马拉车算法 Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,由一个叫 Manacher 的人在 1975 年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性。
详见:马拉车算法
3. 代码
3.1 暴力法
时间复杂度: O ( n 3 ) O(n^{3}) O(n3);空间复杂度: O ( 1 ) O(1) O(1)
//1.暴力法
public static String solution(String s) {
if (s == null)
return null;
int len = s.length();
String res = s.substring(0,1);
for(int i = 0; i < len; i++) {
for (int j = i+1; j <= len; j++) {
String subStr = s.substring(i,j);
if (isPalindrome(subStr))
res = (subStr.length() > res.length()) ? subStr : res;
}
}
return res;
}
private static boolean isPalindrome(String s) {
int n = s.length();
for (int i = 0; i < n / 2; i++) {
if (s.charAt(i) != s.charAt(n - i - 1))
return false;
}
return true;
}
3.2 最长相同子串法
时间复杂度: O ( n 2 ) O(n^{2}) O(n2);空间复杂度: O ( n ) O(n) O(n).
//2.最长相同子串法
public static String solution2(String s) {
if (s == null)
return null;
int len = s.length();
String origin = s;
String reverse = new StringBuilder(s).reverse().toString();
int[][] arr = new int[len][len];
int maxLen = 0;
int maxEnd = 0;
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
if (origin.charAt(i) == reverse.charAt(j)) {
if (i == 0 || j == 0)
arr[i][j] = 1;
else
arr[i][j] = arr[i-1][j-1] + 1;
}
if (arr[i][j] > maxLen) {
int beforeRev = len - j - 1;
if (beforeRev + arr[i][j] - 1 == i) {
maxLen = arr[i][j];
maxEnd = i;
}
}
}
}
return s.substring(maxEnd - maxLen + 1, maxEnd + 1);
}
//3.最长子串优化版
public static String solution3(String s) {
if (s == null || s.length() == 0)
return null;
int len = s.length();
String origin = s;
String reverse = new StringBuilder(s).reverse().toString();
int[] arr = new int[len];
int maxLen = 0;
int maxEnd = 0;
for (int i = 0; i < len; i++) {
for (int j = len - 1; j >= 0; j--) {
if (origin.charAt(i) == reverse.charAt(j)) {
if (i == 0 || j == 0)
arr[j] = 1;
else
arr[j] = arr[j-1] + 1;
}
else
arr[j] = 0; //之前使用二维数组,不需要置零,现在需要。
if (arr[j] > maxLen) {
int beforeRev = len - j - 1;
if (beforeRev + arr[j] - 1 == i) {
maxLen = arr[j];
maxEnd = i;
}
}
}
}
return s.substring(maxEnd - maxLen + 1, maxEnd + 1);
}
3.3 暴力法的优化(采用动态规划)
时间复杂度: O ( n 2 ) O(n^{2}) O(n2);空间复杂度: O ( n ) O(n) O(n)
//4.暴力法的优化(采用动态规划)
public static String solution4(String s) {
if (s == null || s.length() == 0)
return s;
int len = s.length();
String res = s.substring(0,1);
boolean[][] p = new boolean[len][len];
for (int i = 1; i <= len; i++) { //i表示考察的字符串的长度
for (int start = 0; start < len; start++) {
int end = start + i - 1;
//超出索引范围
if (end >= len)
break;
//单独判断长度为1和2的情况
if (i == 1 || (i == 2 && s.charAt(start) == s.charAt(end)))
p[start][end] = true;
else
p[start][end] = (s.charAt(start) == s.charAt(end)) && p[start+1][end-1];
if (p[start][end] == true)
res = (end - start + 1 > res.length()) ? s.substring(start, end + 1) : res;
}
}
return res;
}
//5.暴力解法的优化(动态规划写法二)
public static String solution5(String s) {
if (s == null || s.length() == 0)
return s;
int len = s.length();
String res = "";
boolean[][] p = new boolean[len][len];
for (int i = len - 1; i >= 0; i--) {
for (int j = i; j < len; j++) {
p[i][j] = (s.charAt(i) == s.charAt(j)) && (j - i < 2 || p[i+1][j-1]); //j-i < 2表示长度为1和2的情况
if (p[i][j] == true)
res = (j - i + 1 > res.length()) ? s.substring(i, j + 1) : res;
}
}
return res;
}
//6.暴力解法的优化(动态规划写法二)的优化
public static String solution6(String s) {
if (s == null || s.length() == 0)
return s;
int len = s.length();
String res = "";
boolean[] p = new boolean[len];
for (int i = len - 1; i >= 0; i--) {
for (int j = len - 1; j >= i; j--) {
p[j] = (s.charAt(i) == s.charAt(j)) && (j - i < 2 || p[j-1]); //j-i < 2表示长度为1和2的情况
if (p[j] == true)
res = (j - i + 1 > res.length()) ? s.substring(i, j + 1) : res;
}
}
return res;
}
3.4 中心扩展算法
时间复杂度: O ( n 2 ) O(n^{2}) O(n2);空间复杂度: O ( 1 ) O(1) O(1)
//7.中心扩展算法
public static String solution7(String s) {
if (s == null || s.length() <= 1)
return s;
int len = s.length();
int start = 0;
int end = 0;
for (int i = 0; i < len; i++) {
int len1 = expandAroundCenter(s, i,i);
int len2 = expandAroundCenter(s, i, i+1);
int maxLen = Math.max(len1, len2);
if (end - start < maxLen) {
start = i - (maxLen-1)/2;
end = i + maxLen/2;
}
}
return s.substring(start, end+1);
}
private static int expandAroundCenter(String s, int left, int right) {
int p1 = left, p2 = right;
while (p1 >= 0 && p2 < s.length() && s.charAt(p1) == s.charAt(p2)) {
p1--;
p2++;
}
return p2 - p1 - 1;
}
3.5 Manacher算法
时间复杂度: O ( n ) O(n) O(n);空间复杂度: O ( n ) O(n) O(n)
//8.马拉车算法
public static String solution8(String s) {
if (s == null || s.length() <= 1)
return s;
//第一步预处理
String t = "$";
for (int i = 0; i < s.length(); i++)
t += "#" + s.charAt(i);
t += "#@";
//第二步计算数组p、起始索引、最长回文半径
int n = t.length();
int[] p = new int[n];
int id = 0, mx = 0; //id能够延伸到最右端的那个回文子串的中心点位置;mx表示id对于的最右端位置
//最长回文子串长度
int maxLen = -1;
//最长回文子串的中心位置索引
int index = 0;
for (int j = 1; j < n - 1; j++) {
//最关键的逻辑:当回文串能够延伸到的最右端位置mx大于j时,
//取与j对称的点p[2*id-j]的回文串半径与mx-j二者中较小的
//作为p[j]的临时值,之后向左右两边进行扩展,如果扩展成
//功则需要更新mx和id的值
p[j] = mx > j ? Math.min(p[2*id-j], mx-j) : 1;
//向左右两边扩展,扩展右边界
while (t.charAt(j+p[j]) == t.charAt(j-p[j]))
p[j]++;
//如果回文子串的右边界超过了mx,则需要更新mx和id的值
if (mx < p[j] + j) {
mx = p[j] + j;
id = j;
}
//如果回文子串的长度大于maxLength,则更新maxLen和index的值
if (maxLen < p[j] - 1) {
maxLen = p[j] - 1;
index = j;
}
}
//第三步截取字符串,输出结果
int start = (index-maxLen)/2;
return s.substring(start, start + maxLen);
}