LeetCode算法(5)---最长回文子串

题目:

给定一个字符串 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=2
C - 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)。

猜你喜欢

转载自blog.csdn.net/Arthur_____/article/details/88344451