题目:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
解决方案
暴力解法
思路
暴力法将选出所有子字符串可能的开始和结束位置,并检验它是不是回文。
【基本思路】构架一个HashMap存储<字符,索引>,对字符串从头到尾遍历一遍,不在Map里的字符存进去,在的话比较上一次出现的位置,若是索引之间间隔小于3,产生回文,对其进行进一步处理,最后更新字符位置,保证Map里的字符出现的位置都是最大的。
判断有一个回文出现后,直接调用String本身来判断回文的长度和数据,而不是使用Map。Map的唯一作用是找到第一个回文字符对,随后将会获取从该字符对产生的最大回文。
之所以Map索引的更新不回对下一个回文造成影响,是因为Map的作用只是找到第一对回文的字符地址。
public static String longestPalindrome(String s) {
Map<Character,Integer> map = new HashMap<>();
int length = s.length();
int index = 0;//索引
int bigLen = 0;
String subString = "";
if(length!=0){
subString += s.charAt(0);
bigLen=1;
}
while(length>index){
//基本思路是找到一个回文则在当前循环内计算完这个回文的信息
//解决Map索引不能重复的思路:但发现有回文的时候,判断回文的最长信息是直接条用字符串本身,而不是分析Map中存储的数据;
//Map中的数据的作用只是为下一个回文做准备、
if(map.containsKey(s.charAt(index))){
//计算是否是回文
int lastIndex = map.get(s.charAt(index));//上一个相同字符
if(index-lastIndex<3){
//产生回文,取前一个和后一个的索引
int before = lastIndex-1;
int after = index+1;
if((index-lastIndex)==1){
//此时两个相同字符相邻,可能会出现多个相同字符相邻的情况
while(before>=0 && s.charAt(index)==s.charAt(before)){
//处理方法是判断前一个字符是否和当前字符相同,是则再往前一个
before--;
}
}
while(before>=0 && after<length && s.charAt(before)==s.charAt(after)){
before--;
after++;
}
if(bigLen < (after-before-1)){
subString = s.substring(before+1,after);
bigLen = after - before - 1;
}
}
map.put(s.charAt(index),index);
}else{
map.put(s.charAt(index),index);
}
index++;
}
return subString;
}
复杂度分析(暴力法)
时间复杂度:O(n3),假设 n 是输入字符串的长度,则 n(n-1) / 2 为此类子字符串(不包括字符本身是回文的一般解法)的总数。因为验证每个子字符串需要 O(n) 的时间,所以运行时间复杂度是 O(n3)。
空间复杂度:O(1)。
Manacher算法(马拉车算法)
一个神奇的算法、、、
参考文章https://www.jianshu.com/p/116aa58b7d81中有详细解释
基本思路:
设置以下几个参数:
(1)回文半径数组radius[]
(2)最右回文 的 右边界R
(3)最右回文 的 对称中心C
首先,处理字符串,在字符的中间加一个固定的字符(如"#"),能避免奇数和偶数的问题。
接着分文两大类处理:
第一大类,下一位置(index)在R右边(index>R):
此时以index为中心(C),向两边扩展(即普通算法)。
更新半径数组(radius);
更新R;
第二大类,下一位置(index)不在R的右边(index<=R):
此时建立几个中间变量:
(1)最右回文的左边界L(L=2C-R)
(2)index关于对称中心的对称位置p2(p2=2C - index)
(3)以p2为中心的最右回文的左边界p2L(p2L=p2-radius[p2]+1)
又分为三种情况:
1、p2L>L:
此时当前位置的回文半径 radius[index] 与 p2 相同
radius[index] = radius[p2]
2、p2L<L:
此时当前位置的回文半径 radius[index] 就是index到R的距离:R-index+1
radius[index] = R - index + 1
3、p2L=L:
此时当前位置的的回文半径 radius[index] 就还要继续往外扩,但是只需要从R之后往外扩就可以了
外之之后更新R和C即可
//方法2:马拉车算法
//首先处理字符串,中间加#,并将其转换成char数组,方便运算
public static char[] manageString(String s) {
StringBuilder sb = new StringBuilder();
for(int i =0; i < s.length();i++) {
sb.append("#");
sb.append(s.charAt(i));
}
sb.append("#");
return sb.toString().toCharArray();
}
//然后写核心代码
public static String longestPalindrome2(String s) {
//若为空直接返回
if (s == "")return "";
char[] strArray = manageString(s);
System.out.print("转换好的字符串为:[");
for(int i = 0 ; i < strArray.length ; i++) {
System.out.print(strArray[i]);
}
System.out.println("]");
int[] radius = new int[strArray.length];//回文半径数组
int R = -1;//最右回文 的 右边界R
int C = -1;//最右回文 的 对称中心C
int maxRadius = 0;
int maxC = -1;
int length = strArray.length;
for(int index = 0; index < length; index++) {
if (index > R) {
C = index;
int j = 1;
//要加一个上下界判断
while( (index+j)<=length -1 && (index-j)>=0 && strArray[index-j] == strArray[index+j]) {
j++;
}
radius[index] = j;//更新半径数组
R = C + j -1;//更新右边界R
}else {
//剩下的就是下一位置在R左边(或等于R),分三种情况处理
int L = 2*C - R;//最右回文 的 左边界L
int p2 = 2*C - index;//当前位置 关于对称中心C 的 对称点p2
int p2L = p2 - radius[p2] +1;//以p2点为中心的 回文 的左边界p2L
if(p2L > L) {
//第一种情况
radius[index] = radius[p2];
}else if(p2L < L) {
//第二种情况
radius[index] = R - index + 1;
}else {
//第三种情况(相等)
//以当前位置index为中心开始扩充
C = index;
int j = R - index + 1;
while( (index+j)<=length -1 && (index-j)>=0 && strArray[index-j] == strArray[index+j]) {
j++;
}
radius[index] = j;//更新半径数组
R = C + j -1;//更新右边界R
}
}
if(radius[index]>maxRadius) {
maxRadius = radius[index];
maxC = C;
}
}
System.out.print("回文半径数组为:[");
for(int i = 0;i<length;i++) {
System.out.print(radius[i]);
}
System.out.println("]");
return getTheString(strArray,maxRadius,maxC);
}
//再写一个反处理数组的方法
public static String getTheString(char[] charArray, int maxRadius,int maxC) {
//字符数组中的奇数才是所需要的字符
StringBuilder sb = new StringBuilder();
for(int i = maxC - maxRadius + 1; i < maxC + maxRadius ;i++) {
if (i%2==1) {
sb.append(charArray[i]);
}
}
return sb.toString();
}
复杂度分析
从上面的分析中,可以看出,第二大类的1,2两种情况的求某个位置的回文半径的时间复杂度是O(1);
对于第一大类和第二大类的情况3,R是不断的向外扩的,不会往回退,而且寻找回文半径时,R之内的位置是不是进行判断的,所以对整个字符串而且,R的移动是从字符串的起点移动到终点,时间复杂度是O(n),所以整个manacher的时间复杂度是O(n)。