5、Longest|5、Longest Palindromic Substring

【5、Longest|5、Longest Palindromic Substring】经典的最长回文子串问题,有很多种解法,这里列出两到三种。
Example

Input: "babad" Output: "bab" Input: "cbbd" Output: "bb"

解法一:
动态规划
思路
根据回文的特性,一个大回文按比例缩小后的字符串也必定是回文,比如ABCCBA,那BCCB肯定也是回文。所以我们可以根据动态规划的两个特点:第一大问题拆解为小问题,第二重复利用之前的计算结果,来解答这道题。
1、先把所有长度最短为1的子字符串计算出来,根据起始位置从左向右,这些必定是回文。
2、计算所有长度为2的子字符串,再根据起始位置从左向右。
3、到长度为3的时候,就可以利用上次的计算结果:如果中心对称的短字符串不是回文,那长字符串也不是,如果短字符串是回文,那就要看长字符串两头是否一样。
这样,一直到长度最大的子字符串,我们就把整个字符串集穷举完了,但是由于使用动态规划,使计算时间从O(N3)减少到O(n2)。
注意
外循环的变量控制的实际上不是字符串长度,而是字符串首到尾的增量
二维数组的第一维是指子字符串起始位置,第二维是指终止位置,所存数据表示是否回文
public class Solution { public String longestPalindrome(String s) { int maxLength = 0; int maxStart = 0; int len = s.length(); boolean[][] dp = new boolean[len][len]; //i是字符串长度 for(int i = 0; i < len; i++){ //j是字符串起始位置 for(int j = 0; j < len - i; j++){ if(i==0||i==1){ //如果字符串长度为0,必定为回文 dp[j][j+i] = true; } else if(s.charAt(j+i)==s.charAt(j)){ //如果左右两端相等,那只要中心对称子字符串是回文就是回文 dp[j][j+i] = dp[j+1][j+i-1]; } else { //否则不是回文 dp[j][j+i] = false; } if(dp[j][j+i] && i > maxLength){ maxLength = i + 1; maxStart = j; } } } return s.substring(maxStart,maxStart + maxLength); } }

解法二:
中心扩散法
复杂度
时间 O(n^2) 空间 O(1)
思路
动态规划虽然优化了时间,但也浪费了空间。实际上我们并不需要一直存储所有子字符串的回文情况,只需要知道中心对称的较小一层是否是回文。所以如果我们从小到大连续以某点为个中心的所有子字符串进行计算,就能省略这个空间。
这种解法中,外层循环遍历的是子字符串的中心点,内层循环则是从中心扩散,一旦不是回文就不再计算其他以此为中心的较大的字符串。
由于中心对称有两种情况,一是奇数个字母以某个字母对称,而是偶数个字母以两个字母中间为对称,所以我们要分别计算这两种对称情况。
public class Solution { String longest = ""; public String longestPalindrome(String s) { for(int i = 0; i < s.length(); i++){ //计算奇数子字符串 helper(s, i, 0); //计算偶数子字符串 helper(s, i, 1); } return longest; }private void helper(String s, int idx, int offset){ int left = idx; int right = idx + offset; while(left>=0 && right longest.length()){ longest = currLongest; } } }

解法三:
马拉车算法
首先用一个非常巧妙的方式,将所有可能的奇数/偶数长度的回文子串都转换成了奇数长度:在每个字符的两边都插入一个特殊的符号。比如 abba 变成 #a#b#b#a#, aba变成 #a#b#a#。 为了进一步减少编码的复杂度,可以在字符串的开始加入另一个特殊字符,这样就不用特殊处理越界问题,比如$#a#b#a#。
public class Solution { public String longestPalindrome(String s) { if(s.length()<=1){ return s; } // 预处理字符串,避免奇偶问题 String str = preProcess(s); // idx是当前能够向右延伸的最远的回文串中心点,随着迭代而更新 // max是当前最长回文串在总字符串中所能延伸到的最右端的位置 // maxIdx是当前已知的最长回文串中心点 // maxSpan是当前已知的最长回文串向左或向右能延伸的长度 int idx = 0, max = 0; int maxIdx = 0; int maxSpan = 0; int[] p = new int[str.length()]; for(int curr = 1; curr < str.length(); curr++){ // 找出当前下标相对于idx的对称点 int symmetryOfCurr = 2 * idx - curr; // 如果当前已知延伸的最右端大于当前下标,我们可以用对称点的P值,否则记为1等待检查 p[curr] = max > curr? Math.min(p[symmetryOfCurr], max - curr):1; // 检查并更新当前下标为中心的回文串最远延伸的长度 while((curr+p[curr])max){ max = p[curr] + curr; idx = curr; } // 检查并更新当前已知的最长回文串信息 if(p[curr]>maxSpan){ maxSpan = p[curr]; maxIdx = curr; } } //去除占位符 return s.substring((maxIdx-maxSpan)/2,(maxSpan+maxIdx)/2-1); }private String preProcess(String s){ // 如ABC,变为$#A#B#C# StringBuilder sb = new StringBuilder(); sb.append("$"); for(int i = 0; i < s.length(); i++){ sb.append("#"); sb.append(s.charAt(i)); } sb.append("#"); return sb.toString(); } }

    推荐阅读