最长回文子串(优化思路较难理解)(5. 最长回文子串)
题目:
思路:
【1】先说说暴力破解,基于双循环取出所有的可以分割的字符串,然后判断是否是回文数,是的话记录长度是否比已知的还要长,是的话,则记录长度。暴力破解本身思想很简单,但是做的无用功也多,因为每个字符串都要处理一遍,然而很多字符串其实都是不需要的,那么减少要处理的字符串就是优化的重点。
【2】基于扩散思维的处理,首先在暴力破解里面我们写了对回文字符串的判断,那么回文字符串其实有两种情况,奇数串和偶数串,如:ABA和ABBA。那么便可以使用单循环替代双循环,遍历每一个字符串,查看它的扩散情况,最优的情况就是一开始就扩散不动,那么直接返回,跳过一大波的字符串,另一种就是最坏情况,扩散到边界。如aaabaaaa,当下标指向b的时候,扩散到极致便是aaabaaa,如aacbaaaa,当下标指向b的时候,一开始就扩散不动,那么就会直接跳过一大波暴力破解中分割出来的字符串。【总体而言就是筛选】
【3】Manacher(马拉车)算法的处理,本质上就是觉得中心扩散中处理的字符串还是太多了,能不能继续跳过一部分。又由于回文字符串有两种情况,不好处理,直接转为奇数串的形式。然后如aaabaaaa,会被置为#a#a#a#b#a#a#a#a#,如当遍历过b的时候其实就会将b置为当前最大的回文字符串中心下标7,且半径为8,所以得到的最右边的边界为14,所以当遍历到下标为8的a的时候先判断是否在容纳范围内,是的话先取出关于当前最大回文字符串中心下标的对称点的半径,如果它的半径范围也没有超出中心下标的边界,那么其实可以不用遍历了,因为必定是会被包含的。如果超出了,那么其实还是要走中心扩散的路子,自行找出最以该字符串进行中心扩散的情况,取出后要判断是否会对当前最大回文字符串进行替换。
代码展示:
基于暴力破解的方式:
//执行用时:304 ms, 在所有 Java 提交中击败了7.78%的用户 //内存消耗:41.5 MB, 在所有 Java 提交中击败了74.94%的用户 class Solution { public String longestPalindrome(String s) { if (s.length() < 2) { return s; } int start = 0; int maxLen = 0; for (int i = 0; i < s.length() - 1; i++) { for (int j = i; j < s.length(); j++) { // 截取所有子串,如果截取的子串小于等于之前遍历过的最大回文串,直接跳过。 // 因为截取的子串即使是回文串也不可能是最大的,所以不需要判断 if (j - i < maxLen) { continue; } if (isPalindrome(s, i, j)) { if (maxLen < j - i + 1) { start = i; maxLen = j - i + 1; } } } } return s.substring(start, start + maxLen); } //判断是否是回文串 private static boolean isPalindrome(String s, int start, int end) { while (start < end) { if (s.charAt(start++) != s.charAt(end--)) { return false; } } return true; } }
基于暴力破解的优化【中心扩散】:
//执行用时:20 ms, 在所有 Java 提交中击败了72.27%的用户 //内存消耗:41.1 MB, 在所有 Java 提交中击败了92.68%的用户 class Solution { public String longestPalindrome(String s) { String res = ""; for (int i = 0; i < s.length(); i++) { String s1 = isPalindrome(s, i, i); String s2 = isPalindrome(s, i, i + 1); res = res.length() > s1.length() ? res : s1; res = res.length() > s2.length() ? res : s2; } return res; } //直接向两边扩散取出最长回文字符串 public String isPalindrome(String s, int l, int r) { while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { l--; r++; } return s.substring(l + 1, r); } }
最优解法【Manacher(马拉车)算法】:
//执行用时:3 ms, 在所有 Java 提交中击败了98.18%的用户 //内存消耗:41.3 MB, 在所有 Java 提交中击败了80.14%的用户 class Solution { public String longestPalindrome(String s) { int charLen = s.length();//源字符串的长度 int length = charLen * 2 + 1;//添加特殊字符之后的长度 char[] chars = s.toCharArray();//源字符串的字符数组 char[] res = new char[length];//添加特殊字符的字符数组 int index = 0; //添加特殊字符 for (int i = 0; i < res.length; i++) { res[i] = (i % 2) == 0 ? '#' : chars[index++]; } //新建p数组 ,p[i]表示以res[i]为中心的回文串半径 int[] p = new int[length]; //maxRight(某个回文串延伸到的最右边下标) //maxCenter(maxRight所属回文串中心下标), //resCenter(记录遍历过的最大回文串中心下标) //resLen(记录遍历过的最大回文半径) int maxRight = 0, maxCenter = 0, resCenter = 0, resLen = 0; //遍历字符数组res for (int i = 0; i < length; i++) { if (i < maxRight) { //情况一,i没有超出范围[left,maxRight] //2 * maxCenter - i其实就是j的位置,实际上是判断p[j]<maxRight - i if (p[2 * maxCenter - i] < maxRight - i) { //j的回文半径没有超出范围[left,maxRight],直接让p[i]=p[j]即可 p[i] = p[2 * maxCenter - i]; } else { //情况二,j的回文半径已经超出了范围[left,maxRight],我们可以确定p[i]的最小值 //是maxRight - i,至于到底有多大,后面还需要在计算 p[i] = maxRight - i; while (i - p[i] >= 0 && i + p[i] < length && res[i - p[i]] == res[i + p[i]]) p[i]++; } } else { //情况三,i超出了范围[left,maxRight],就没法利用之前的已知数据,而是要一个个判断了 p[i] = 1; //首先(i - p[i])和(i + p[i])为双指针,所以应在数组下标范围内,不得出现数组越界问题 //其次,比较两者相同则双指针各移动一位 while (i - p[i] >= 0 && i + p[i] < length && res[i - p[i]] == res[i + p[i]]) p[i]++; } //匹配完之后,如果右边界i + p[i]超过maxRight,那么就更新maxRight和maxCenter if (i + p[i] > maxRight) { maxRight = i + p[i]; maxCenter = i; } //记录最长回文串的半径和中心位置 if (p[i] > resLen) { resLen = p[i]; resCenter = i; } } //计算最长回文串的长度和开始的位置 resLen = resLen - 1; int start = (resCenter - resLen) >> 1; //截取最长回文子串 return s.substring(start, start + resLen); } }