数据结构–最长回文串
输入一个字符串,返回其最长的回文串
回文:正着和倒着是相同的字符串
方法一:中心回文(奇偶合一)
分析
不妨假设这个字符串的长度为4,然后其回文中心可能是同一个点(奇数),也可能是两个点(偶数),而看下图,我们可以使用统一的式子,从 i = 0 开始遍历,当 i < 字符串的长度 x 2 - 1 时,结束遍历,而 left = i / 2,right = i / 2 + i % 2;
然后通过向左右扩展,记录索引和长度,最后把长度和目前的最大长度比较来更新最长字符串的起始位置和长度
代码
class Solution {
public String longestPalindrome(String s) {
int length = s.length();
int maxLength = 0;//最大字符串的长度
int leftIndex = 0;//返回字符串的左边起始位置
int rightIndex = 0;//返回字符串的右边起始位置
for (int i = 0; i < 2 * length - 1; i++) {
int left = i / 2;//左回文中学
int right = i / 2 + i % 2;//右回文中心
int record = 0;//记录每个回文中心的回文字符串长度
while (left >= 0 && right < length && s.charAt(left) == s.charAt(right)){
--left;
++right;
++record;
}
if (right - left - 1 > maxLength){
//和最长回文串长度比较,更新索引和最大长度
maxLength = right - left - 1;
//因为最后两边索引完成了自增和自减,计算长度和索引要注意
rightIndex = right - 1;
leftIndex = left + 1;
}
}
return s.substring(leftIndex,rightIndex + 1);
}
}
方法二:中心回文(分奇偶讨论)
分析
正常遍历 n - 1 次,但是每个 i 都计算了在该点(奇数,两个都在同一点)和一个在该点与另一个在下一点(偶数)的长度,再分别判断长度与最长字符串长度的关系来更新最长字符串,这里使用的字符串长度来求出索引位置
代码
public String longestPalindrome(String s) {
if (s == null || s.length() < 2) return s;
int resIdx = 0, resLen = 1;
for (int idx = 0; idx < s.length() - 1; idx++) {
//奇数最长回文长度
int len1 = longestLen(s, idx, idx);
//偶数最长回文长度
int len2 = longestLen(s, idx, idx + 1);
if (len1 > resLen) {
resLen = len1;// 更新最长字符串长度
resIdx = idx - resLen / 2;
// 记录初始的位置
}
if (len2 > resLen) {
resLen = len2;// 更新最长字符串长度
resIdx = idx - (resLen / 2 - 1);
// 记录初始的位置
}
}
return s.substring(resIdx, resIdx + resLen);
}
//左右探索最长回文子串长度
private int longestLen(String s, int left, int right) {
while (left >= 0 && right < s.length()) {
if (s.charAt(left) != s.charAt(right)) break;
left--;
right++;
}
return right - left - 1;
}
方法三;dp
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
boolean[][] dp = new boolean[len][len];
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
int resIdx = 0, resLen = 1;
for (int j = 1; j < len; j++) {
for (int i = 0; i < j; i++) {
if (s.charAt(i) != s.charAt(j)) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
if (dp[i][j] && j - i + 1 > resLen) {
resIdx = i;
resLen = j - i + 1;
}
}
}
return s.substring(resIdx, resIdx + resLen);
}
方法四:manacher算法
返回最长回文串的长度
这个是通过manacher算法,求出最长回文串的长度
class Solution {
public int countSubstrings(String s) {
int n = s.length();
StringBuffer t = new StringBuffer("$#");
for (int i = 0; i < n; ++i) {
t.append(s.charAt(i));
t.append('#');
}
n = t.length();
t.append('!');
int[] f = new int[n];
int iMax = 0, rMax = 0, ans = 0;
for (int i = 1; i < n; ++i) {
// 初始化 f[i]
f[i] = i <= rMax ? Math.min(rMax - i + 1, f[2 * iMax - i]) : 1;
// 中心拓展
while (t.charAt(i + f[i]) == t.charAt(i - f[i])) {
++f[i];
}
// 动态维护 iMax 和 rMax
if (i + f[i] - 1 > rMax) {
iMax = i;
rMax = i + f[i] - 1;
}
// 统计答案, 当前贡献为 (f[i] - 1) / 2 上取整
ans += f[i] / 2;
}
return ans;
}
}
返回最长回文串
分析
在前面我们知道对于中心拓展法(中心回文)需要讨论奇偶来确定回文中心,而对于任意一个字符串,我们可以通过插空插入#来完成转换为奇数
偶数情况:
奇数情况:
核心原理:
先假设已经确定图中c点为最右回文串中心,如图所示的从中间的c到最右边的a为回文半径
i 表示该点的索引
-
遍历到第一个a,关于c的对称点的回文半径为1,小于最右边a到它的回文半径,所以初始回文半径就是1,然后扩张后,发现不能再扩张
-
然后遍历到如下图所示的b位置,其关于c的对称点的回文半径为2,小于最右边a到b的回文半径4,所以b的初始回文半径为2,那 i + 2 表示的就是下图的倒数第一个b点,i - 2 表示的是中心的c点,不满足回文,所以该点的回文半径就是2
(因为这是包含在最右回文串里面的,如果上面的 i + 2 和 i - 2 是对称的,假设倒数第一个b变为c,那因为包含在最右回文串里,所以第一个b也为c,那b关于c的对称点左边那个索引就应该到回文半径就为3)下面第二张图,所以其实只要包含在最右回文串里面,它的回文半径就是对应对称点的回文半径
遍历到如下图索引的a点,我们可以发现,其关于对称点的回文半径为3,已经达到最右回文串的边界,此时i就图中a的索引,i + 3 ,与 i - 3 分别对应是最外面的c和中心位置的c,我们发现它是对称的,因为超出边界了,所以要重新开始判断是否对称,之后还要判断其与最大回文串之间的长度关系,还有最右边界是否需要更新,所以这种情况我们的初始回文半径是3
这是遍历到b点情况,此时对称点的回文半径(4)已经超过边界到其的回文半径(2)了,因为超出部分没有在最右回文串里面,所以需要重新判断,初始回文半径为边界到其的回文半径(2),然后 i + 2 与 i - 2 是图中Y的位置,不满足对称,而b点左边的对称点,其X位置是对称的
总结:边界超出最右回文串的部分需要重新判断,而在里面的不需要,所以我们确认的初始回文半径就是对称点的回文半径和最右边界到该点回文半径的最小值
代码
package String;
public class reverse3 {
public String longestPalindrome(String s) {
int length = s.length();// 当前字符串的长度
// 起始添加“$#”,因为要满足整个字符串最后不会越界
// 即起始不会相同所以第一项和最后一项分别赋值$和!
StringBuilder expandStr = new StringBuilder("$#");
for (int i = 0; i < length; ++i) {
expandStr.append(s.charAt(i));
expandStr.append('#');
}// 完成对字符串的插入
// 把新得到的字符串的长度赋给length
length = expandStr.length();
// 如前文所说,添加!,保证起始不相同,不会越界
expandStr.append('!');
// 创建一个数组来保存字符串每一个元素的回文半径
int [] dp = new int[length];
/**
* 注意:回文半径包括当前的中心元素
* max_Center 用来记录最长回文串的索引
* max_Radius 用来记录最长回文串的回文半径
* right_Center 用来表示当前最右边回文字符串的回文中心(就是其索引)
* right_Radius 用来记录当前最右边回文字符串的右边界
* radius 表示初始回文半径
*/
int max_Center = 0;
int max_Radius = 0;
int right_Center = 0;
int right_Radius = 0;
for (int i = 1; i < length; ++i) {
// i 表示当前元素的索引
// 得到当前的元素的初始回文半径长度,分为两种大情况
// 1. 当前元素在最右的回文串的边界内(下面两种分类,主要是为了区分是否需要进行回文串更新)
// 1.1 因为在最右的回文串里,我们可以找到其关于最右回文串的回文中心(right_Center)的对称点
// 若其对称点的回文半径小于 right_Radius - i + 1 (表示的边界到该元素的半径,包含该点)
// 那么其radius就为对称点的回文半径
// 注意:当前元素索引 + 对称点索引 = 2 * right_Center;则对称点的索引为2 * right_Center - i
// 这种情况是不会更新最右的回文串的,因为其大前提是在最右回文串内
// 1.2 若其对称点的回文半径大于 right_Radius - i + 1 (表示的边界到该元素的半径,包含该点)
// 那么我们就让radius为 right_Radius - i + 1
// 此时再进行向外扩张,判断出界部分是不是满足对称回文
// 2. 当前元素在最右的回文串的边界外
// 2.1 此时回文半径就是1(就是这个元素本身)
int radius = i < right_Radius? Math.min(dp[2 * right_Center - i],right_Radius - i + 1):1;
// 判断是否满足对称
/**
* i + radius 该回文串的右一位
* i - radius 该回文串的左边一位
* 若 xbdadbc,则 $#x#b#d#a#d#b#c#!可知a为回文中心索引8,回文半径为6
* 那么对于 i + radius,i - radius分别就是c,x
* 他们对于上述两种情况刚好保证是初始回文半径的两端两位
* 刚好进行比较
*/
while (expandStr.charAt(i + radius) == expandStr.charAt(i - radius)){
++radius;
}
// 将这个元素的最大回文半径,存入这个索引对应的数组(因为后面的对称点可以对称查找对称点的回文半径)
dp[i] = radius;
// 如果该点的回文半径比最大的回文半径大,则更新最大回文半径和回文中心
if (dp[i] > max_Radius){
max_Radius = dp[i];
max_Center = i;
}
// i + dp[i] - 1 表示该回文串的最右边界,如果大于目前的最右回文边界,则更新最右回文中心(用来找对称点),最右回文边界(用来判断是否出界)
if (i + dp[i] - 1 > right_Radius){
right_Radius = i + dp[i] - 1;
right_Center = i;
}
}
// 根据奇偶,回文串是偶,则回文中心是#,为奇,则回文中心是其中一个元素
// 根据奇偶和数学归纳,可以到原字符串来截取
return expandStr.charAt(max_Center) == '#'? s.substring(max_Center / 2 - max_Radius / 2,max_Center / 2 + max_Radius / 2 ) : s.substring(max_Center / 2 - max_Radius / 2 ,max_Center / 2 + max_Radius / 2 - 1);
}
}