题目
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
代码
解法 1: 暴力破解
暴力求解,列举所有的子串,判断是否为回文串,保存最长的回文串。
// 暴力解法
public String longestPalindrome(String s) {
String ans = "";
int max = 0;
int len = s.length();
for (int i = 0; i < len; i++)
for (int j = i + 1; j <= len; j++) {
String test = s.substring(i, j);
if (isPalindromic(test) && test.length() > max) {
ans = s.substring(i, j);
max = Math.max(max, ans.length());
}
}
return ans;
}
private boolean isPalindromic(String s) {
int len = s.length();
for (int i = 0; i < len / 2; i++) {
if (s.charAt(i) != s.charAt(len - i - 1)) {
return false;
}
}
return true;
}
说明,暴力解法可以优化一下,遍历时只记录begin和maxLen,最后返回时再截取字符串。减少一定的性能消耗。
解法2:中心扩散
- 枚举所有可能的回文子串的中心位置
- 中心位置可能是一个字符(奇数回文子串),也有可能是两个相邻的字符(偶数回文子串)。
- 记录最长回文子串的相关变量
奇数回文子串
偶数回文子串
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
char[] charArray = s.toCharArray();
for (int i = 0; i < len - 1; i++) {
int oddLen = expandAroundCenter(charArray, i, i);//奇数回文子串,比如bab,中心为a
int evenLen = expandAroundCenter(charArray, i, i + 1);//偶数回文子串,比如baab,中心为aa
int curMaxLen = Math.max(oddLen, evenLen);
if (curMaxLen > maxLen) {
maxLen = curMaxLen;
// 根据maxLen和下标 i 计算出 begin
// 这一步要在纸上画图发现规律
begin = i - (maxLen - 1) / 2;
}
}
return s.substring(begin, begin + maxLen);
}
/**
*
* @param charArray 原始字符串的字符数组
* @param left 起始左边界
* @param right 起始右边界
* @return 回文串的长度
*/
private int expandAroundCenter(char[] charArray, int left, int right) {
//当left = right的时候,回文中心是一个字符,回文中心是一个字符,回文串的长度是奇数
//当right = left+1的时候,此时回文中心两个字符,回文串的长度是偶数
int L = left, R = right;
int len = charArray.length;
while (L >= 0 && R < len && charArray[L] == charArray[R]) {
L--;
R++;
}
//跳出while循环时,恰好满足s.charAt(i) != s.charAt(j),
//回文串的长度是 R - L + 1 - 2 = R - L - 1
return R - L - 1;
}
再说一下begin = i - (maxLen - 1) / 2
这句。
奇数回文串
偶数回文串
公式
这里
/
\mathbf{/}
/表示下取整。
解法3 : 动态规划
方法一中,存在大量的重复计算工作,例如当 s=“abcba” 时, 对于子串 “bcb” 和 子串 “abcba”, 分别进行了2次完整的计算,来检测该子串是否是回文串。
很明显的是,对于 s=“abcba” , 在已知 "bcb"是回文串的情况下,要判断 "bcb"是否是回文串的话,只需要判断两边的*位置的字符是否相等即可。 我们定义 P(i,j)
表示 s[i,j]
是否是回文串,若s[i,j]
是回文串,则P(i,j)=true
,否则为false
. 则有下面的递推公式成立:
P[i,j] = p(i+1,j-1) && ( s[i]==s[j] )
对于上面公式有2个特殊情况,当子串长度为1或2时,上面公式不成立。我们单独分析这两种情况:
若子串长度为1,即 j==i, 则 P[i,j] = P[i,i] = true;
若子串长为2,即j==i+1, 则 P[i,j] = P[i,i+1] = ( s[i]==s[i+1] )
所以
- 状态:
dp[i][j]
表示子串s[i..j]
是否为回文子串 - 得到状态转移方程:
dp[i][j] = (s[i] == s[j]) and dp[i+1][j-1]
- 边界条件:
j-1-(i+1)+1<2
,整理得j-i<3
- 初始化:
dp[i][i]=true
代码
public String longestPalindrome(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n];
String ans = "";
for (int k = 0; k < n; ++k) {
for (int i = 0; i + k < n; ++i) {
int j = i + k;
if (k == 0) {
dp[i][j] = true;
} else if (k == 1) {
dp[i][j] = (s.charAt(i) == s.charAt(j));
} else {
dp[i][j] = (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]);
}
if (dp[i][j] && k + 1 > ans.length()) {
ans = s.substring(i, i + k + 1);
}
}
}
return ans;
}
注意
- 子串下标为0,子串长度为1,
dp[i,i] = true
; - 子串下标为1,子串长度为2,则
dp[i,j] = dp[i,i+1] = ( s[i]==s[i+1] )
- 子串长度为1,都是
True
,子串长度为2时,要判断。
解法4:Manacher算法
- 中心扩散的原理是:
如果如果当前字符串不是回文串,那以当前字符串为中心的所有串都不是回文串,以此来终止当前字符串的扩散。 - 动态规划的原理是:
如果当前字符串首尾字符相同,如果去掉首尾字符剩下的子串不是回文串,那当前字符串肯定不是回文串,以此来终止当前字符串的缩减。 - 马拉车算法的原理:
按顺序对每个字符进行中心扩散,利用回文串镜像的特点,找到当前字符在包含它的回文串中的镜像字符,利用镜像字符之前的计算结果,来跳过一些中心扩散的步骤,甚至直接得出结果,以此对中心扩散进行优化。
public String longestPalindrome(String s) {
if (s == null || s.length() == 0)
return "";
// 先拓展,加一个标志位,由于符号任取,和别人一样用 # 吧
char[] chars = new char[(s.length() << 1) + 1];
for (int i = 0; i < chars.length; i++)
chars[i] = (i & 1) == 0 ? '#' : s.charAt((i - 1) >> 1);
int longestIdx = 1;
int[] width = new int[chars.length];
for (int i = 1, mid = 1, right = 1; i < chars.length; i++) {
// 保证 right 永远大于等于 i,可以保证 i 永远可以对 mid 取到
// 镜像字符 mirror(至少等于自己)
if (i > right)
right = mid = i;
int left = (mid << 1) - right, mirror = (mid << 1) - i;
width[i] = Math.min(width[mirror], mirror - left);
for (int x = i - width[i] - 1, y = i + width[i] + 1;
x > -1 && y < chars.length && chars[x] == chars[y];
x--, y++)
width[i]++;
longestIdx = width[i] > width[longestIdx] ? i : longestIdx;
if (i + width[i] > right) {
right = i + width[i];
mid = i;
}
}
// 左右必定会扩展到 ‘#’ 的,这样写左边的 # 会对应到原字符串右边的字符,
// 右边的 # 会对应到原字符串左边的字符。
int left = (longestIdx - width[longestIdx] + 1) / 2;
int right = (longestIdx + width[longestIdx] - 1) / 2;
return s.substring(left, right + 1);
}
资料
最长回文子串(longest-palindromic-substring)
windliang资料
LeetCode 5. Longest Palindromic Substring 最长回文子串 C#
Manacher 算法其实很简单
经典算法问题:最长回文子串之 Manacher 算法