题目
动态规划思路:
public String longestPalindrome(String s) {
if (s == null || s.length() == 1) return s;
int n = s.length();
int maxLength = 1;//最⼤⻓度回⽂串的⻓度
int left = 0; //最⼤⻓度回⽂串的起始索引
char[] chars = s.toCharArray();
boolean[][] dp = new boolean[n][n]; //备忘录,矩阵表示该⼦串是否是回⽂(⾏索引为⼦串起点、列索引为⼦串终点)
for (int subLen = 1; subLen <= n; ++subLen) {//⼦串⻓度从1到n
for (int i = 0; i + subLen - 1 < n; ++i) {//遍历每⼀个起点
int j = i + subLen - 1;//终点
if (subLen == 1) {//单字符本身为回⽂
dp[i][j] = true;//
} else if (subLen == 2) {//双字符直接⽐对
dp[i][j] = (chars[i] == chars[j]);
} else {//状态转移⽅程
dp[i][j] = (chars[i] == chars[j] && dp[i + 1][j - 1]);
}
if (dp[i][j] && subLen > maxLength) {//记录当前最⼤回⽂串位置
maxLength = subLen;
left = i;
}
}
}
return new String(chars, left, maxLength);
}
优化思路:
动态规划的解题思路有一些不必要的判断,它是遍历整个dp[n][n]这个地图了,时间复杂度和空间复杂度都是O(n^2),但是当你的最里面的两个字符dp[i][i+1]已经不是回文字符串了,那么以它为中心的字符串就没有判断的必要了,因此以这个思路莱优化我们的代码
中心扩展算法:
public String longestPalindrome(String s) {
if (s == null || s.length() == 1) return s;
char[] chars = s.toCharArray();
int start = 0, maxLen = 1;//当前最⻓回⽂⼦串的起始位置及⻓度
for (int i = 0; i < chars.length; i++) {//遍历每⼀个字符,尝试找到以其为中⼼的最⻓回⽂串
int len1 = expandAroundCenter(chars, i, i);//奇数⻓度,以i为中⼼扩散
int len2 = expandAroundCenter(chars, i, i + 1);//偶数⻓度,以i、i+1为中⼼扩散
int len = Math.max(len1, len2);
if (len > maxLen) {
start = i - (len - 1) / 2;
maxLen = len;//当前最⼤⻓度
}
}
return new String(chars, start, maxLen);
}
//以left、right为中⼼可扩展的最⼤回⽂串⻓度
public int expandAroundCenter(char[] chars, int left, int right) {
while (left >= 0 && right < chars.length && chars[left] == chars[right]) {
left--;
right++;
}
return right - left - 1;
}
最坏的情况下时间复杂度为O(n^2),空间复杂度为O(n);时间相当于动态规划的性能提升了不少
再优化:
问题:1.回文相互重叠的时候,会有大量的重复计算
2.每次都要分别解决奇数和偶数长度的回文串
3.有两层嵌套循环
Manacher算法:
特点:线性时间内查找字符串中任意位置开始的所有回文子串
首先进行预处理
1.解决奇数偶数问题
在每个字符间插入'#'字符,使得每次只用解决偶数的问题
eg: ababa
插入后:#a#b#a#b#a#
2.解决头尾判断越界问题
只要给头尾插入不一样的字符,这样每次遍历到这个特殊字符的时候,就直接结束判断
eg: ababa
插入后:^#a#b#a#b#a#$
3.解决两层嵌套循环
记录每个位置进行中心扩展的最大半径(类似于备忘录)
***通过镜像去填充数组,以达到O(n)的时间复杂度
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() == 1) return s;
//1.转成字符数组,添加特定字符
char[] chars = s.toCharArray();
char[] newChars = preProcess(chars);//预处理后的字符串
int n = newChars.length;//newStr.length();
int[] results = new int[n];//以该索引位置为中⼼的最⻓回⽂⻓度
int center = 0;//初始选取0位置为中⼼
int right = 0;//右边范围,初始为0
//2.遍历新数组,记录每个位置i的最⻓回⽂半径results[i]
for (int i = 1; i < n - 1; i++) { //依次遍历每⼀个位置计算该位置为中⼼的最⻓回⽂⻓度
// 2.1 选定当前中⼼位置center及右边界right,以中⼼位置计算i的镜像位置i_mirror
int i_mirror = 2 * center - i;//以center为中⼼与i对应的左侧镜像位置
// 2.2 如果i不超过右边界, results[i] 取 right – i 与results[i_mirror] 的较⼩值
if (i < right) {
results[i] = Math.min(right - i, results[i_mirror]);//取 镜像的结果 和 i到右边界的距离 中较⼩的值
} else results[i] = 0;//等于 right 的情况
// 2.3 以i为中⼼, results[i]为半径再扩展⽐对,并更新results[i]
while (newChars[i + 1 + results[i]] == newChars[i - 1 -results[i]])
results[i]++;
// 2.4 如果 i+result[i] 超过右边界,则更新 center 和 right (拉⻋)
if (i + results[i] > right) {
center = i;
right = i + results[i];
}
}
//3.遍历results[i]找到最⼤的回⽂半径
int maxLen = 0;
int centerIndex = 0;
for (int i = 1; i < n - 1; i++) {
if (results[i] > maxLen) {
maxLen = results[i];
centerIndex = i;
}
}
//4.换算成原始字符串中起始位置及⻓度,返回最⻓回⽂⼦串
int start = (centerIndex - maxLen) / 2; //将⻢拉⻋下标换算成原字符串下标
return new String(chars,start,maxLen);;
}
/*
字符串预处理,添加分隔符及⾸尾标识
数组⻓度⼤于等于1
*/
public char[] preProcess(char[] chars) {
int n = chars.length, index = 0;
char[] newChars = new char[2 * n + 3];
newChars[index++] = '^';
for (int i = 0; i < n; i++) {
newChars[index++] = '#';newChars[index++] = chars[i];
}
newChars[index++] = '#'; newChars[index++] = '$';
return newChars;
}
}