题目描述 给定一个字符串 s
,找到 s
中最长的回文子串。你可以假设 s
的最大长度为 1000。
示例 1:
输入: "babad" 输出: "bab" 注意: "aba" 也是一个有效答案。
示例 2:
暴力法 扫描一遍字符串,判断 $[i, j]$ 范围内的字符串是不是回文字符串,并且在这个过程中记录最长的字符串
class Solution { public String longestPalindrome (String s) { if (s == null || s.length() < 2 ) { return s; } char [] str = s.toCharArray(); int start = 0 ; int maxLength = 1 ; for (int i = 0 ; i < str.length - 1 ; i++) { for (int j = i + 1 ; j < str.length; j++) { if ((j - i + 1 ) > maxLength) { if (isPalindrome(str, i, j)) { maxLength = j - i + 1 ; start = i; } } } } return s.substring(start, start + maxLength); } private boolean isPalindrome (char [] str, int start, int end) { while (start <= end) { if (str[start] != str[end]) { return false ; } start++; end--; } return true ; } }
动态规划 我们开辟一个数组 $dp$,$dp[i][j]$ 表示 $[i, j]$ 范围内的字符串是否为回文字符串,那我们可以知道
$$ dp[i][j] = \begin{cases} false, \quad &when \quad str[i] \neq str[j] \\ dp[i+1][j-1], \quad &when \quad str[i] = str[j] \end{cases} $$
当 $str[i] \neq str[j]$ 的时候,那么不可能是回文字符串,如果 $str[i] = str[j]$ 的时候,如果 $dp[i+1][j-1]$ 是回文字符串,那么 $dp[i][j]$ 就是回文字符串。
但是如果 $i + 1 > j - 1$ 的话,这时 $dp[i + 1][j - 1]$ 没有意义,其实这种情况就是 $[i,j]$ 内只有两个字符,如果 $str[i] = str[j]$ 的话,那么它就是一个回文字符串,所以这时可以直接得到 $dp[i][j] = true$。
class Solution { public String longestPalindrome (String s) { if (s == null || s.length() < 2 ) { return s; } int length = s.length(); boolean dp[][] = new boolean [length][length]; for (int i = 0 ; i < length; i++) { for (int j = 0 ; j < length; j++) { dp[i][j] = false ; } } for (int i = 0 ; i < length; i++) { dp[i][i] = true ; } int maxLength = 1 ; int begin = 0 ; for (int j = 1 ; j < length; j++) { for (int i = 0 ; i < j; i++) { if (s.charAt(i) != s.charAt(j)) { dp[i][j] = false ; } else { if (i + 1 > j - 1 ) { dp[i][j] = true ; } else { dp[i][j] = dp[i + 1 ][j - 1 ]; } } if (dp[i][j] && (j - i + 1 ) > maxLength) { maxLength = j - i + 1 ; begin = i; } } } return s.substring(begin, begin + maxLength); } }
中间扩散法 上面的情况都是检查某个范围内的字符串是否是回文字符串,而中心扩散法,则是以某点为中心进行向外扩散,在扩散的同时检查是否是回文字符串,过程如下
检查该点左右两边的字符串是否相等,如果相等则继续扩散,如果不相等则停止扩散
停止扩散后记录该回文字符串的长度,如果大于当前记录的最大回文字符串的长度,则进行更新,为了获得该回文字符串,还需要记录该回文字符串的起始位置
需要注意的是,既可以从某个字符开始扩散,也可以从字符间的空隙进行扩散
从某个点进行扩散的代码如下:
private int centerSpread (String s, int left, int right) { int length = 0 ; while (left >=0 && right < s.length()) { if (!(s.charAt(left) == s.charAt(right))) { break ; } length++; left--; right++; } return length; }
如果从某个字符进行扩散,那么 left 和 right 都是该字符的下标,如果从某个字符空隙进行扩散,则 left 和 right 分别为该空隙左右两边字符的下标。扩散的示意图如下
完整代码如下
class Solution { public String longestPalindrome (String s) { int begin = 0 ; int maxLength = 1 ; for (int i = 1 ; i < s.length(); i++) { int length1 = centerSpread(s, i - 1 , i); int length2 = centerSpread(s, i, i); int length = (2 * length1) >= (2 * length2 - 1 ) ? 2 * length1 : (2 * length2 - 1 ); if (length > maxLength) { maxLength = length; begin = i - length/2 ; } } return s.substring(begin, begin + maxLength); } private int centerSpread (String s, int left, int right) { int length = 0 ; while (left >=0 && right < s.length()) { if (!(s.charAt(left) == s.charAt(right))) { break ; } length++; left--; right++; } return length; } }
Manacher算法 Manacher 算法是对中间扩散法的改进,首先我们在字符间填充 #
以此充当字符空隙,得到一个新的字符串,然后用一个数组记录新字符串每个字符能够扩散多远
我们复用中心扩散法的代码
class Solution { public String longestPalindrome (String s) { int begin = 0 ; int maxLength = 1 ; String str = String.join( "#" , s.split("" )); for (int i = 1 ; i < str.length(); i++) { int length = centerSpread(str, i, i); length = 2 * length - 1 ; if (length >= maxLength) { if (length > maxLength || str.charAt(i + length/2 ) != '#' ) { maxLength = length; begin = i - length/2 ; } } } return String.join( "" , str.substring(begin, begin + maxLength).split("#" )); } private int centerSpread (String s, int left, int right) { int length = 0 ; while (left >= 0 && right < s.length()) { if (!(s.charAt(left) == s.charAt(right))) { break ; } length++; left--; right++; } return length; } }
这个时候我们还没有使用上面提到的数组,接下来我们需要利用数组记录一些信息,从而加快中间扩散法的速度。
首先我们要知道如何加快中间扩散法的速度,这就需要利用回文字符串的特点,那就是回文字符串是左右对称 的,当我们对回文字符串内的某个字符进行中间扩散时,我们可以先找到对称位置的字符,看看它能够扩散的多远
如果它扩散的距离小于所处回文字符串的范围,那么根据对称的特点,我们不需要对该字符进行中间扩散,它能够扩散的距离就是对称位置所能扩散的距离
但是对称位置能够扩散的距离刚好在所处回文字符串的边界,或者大于所处的回文字符串,那么此时扩散的距离就不等于对称位置能够扩散的距离,而是需要进行扩散才能确定
但是我们不需要从该字符串开始进行扩散,因为我们可以确定在回文字符串的范围内,它是对称的,所以我们只需要从回文字符串的外部进行扩散即可
扩散完毕后记录能够扩散后的距离,并决定是否更新 begin,maxLength。
现在还有一个问题,如何知道某个字符处于一个回文字符串中,我们通过记录两个变量 center 和 maxRight 来表示一个回文字符串,center 表示回文字符串的中心,maxRight 表示中心离回文字符串右边界的距离
我们认为如果字符的下标小于 $center + maxRight$,那么它就在一个回文字符串中。
那什么时候更新这两个变量呢? 那就是在某个字符扩散之后,他向右边能够扩散的范围大于当前的 $center + maxRight$,那么就更新 center 和 maxRight。
代码如下:
class Solution { public String longestPalindrome (String s) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0 ; i < s.length(); i++) { if (i != s.length() - 1 ) { stringBuilder.append(s.charAt(i) + "#" ); } else { stringBuilder.append(s.charAt(i)); } } String newStr = stringBuilder.toString(); int [] records = new int [newStr.length()]; records[0 ] = 0 ; int begin = 0 ; int maxLength = 1 ; int center = 0 ; int maxRight = 0 ; for (int i = 1 ; i < newStr.length(); i++) { if (i <= center + maxRight) { int mirrorLength = records[2 * center - i]; if (i + mirrorLength >= center + maxRight) { int length = center + maxRight - i; while (i + length + 1 < newStr.length() && (i -length - 1 ) >= 0 ) { if (newStr.charAt(i + length + 1 ) != newStr.charAt(i - length - 1 )) { break ; } length++; } records[i] = length; if (2 * length + 1 >= maxLength) { if (2 * length + 1 > maxLength || newStr.charAt(center) != '#' ) { begin = i - length; maxLength = 2 * length + 1 ; } } if (i + length > center + maxRight) { center = i; maxRight = length; } } else { records[i] = mirrorLength; } } else { int length = 0 ; while (i + length + 1 < newStr.length() && (i - length - 1 ) >= 0 ) { if (newStr.charAt(i + length + 1 ) != newStr.charAt(i - length - 1 )) { break ; } length++; } if (2 * length + 1 > maxLength) { begin = i - length; maxLength = 2 * length + 1 ; } center = i; maxRight = length; records[i] = length; } } String ss = newStr.substring(begin, begin + maxLength); StringBuilder stringBuilder1 = new StringBuilder(); for (int i = 0 ; i < ss.length(); i++) { if (ss.charAt(i) != '#' ) { stringBuilder1.append(ss.charAt(i)); } } return stringBuilder1.toString(); } }