题目描述
最近发现好多童鞋在刷题,偶然看到求最长回文子串的问题,也来尝试下。下面是力扣(LeetCode)上的题目描述:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"`
解法一:暴力法
我一开始还真没想到用这个方法,它是比较简单暴力的。思路是遍历每一个子字符串,判断是否是回文串,如果是就将其长度和之前最大的回文串长度比较,从而得到最长的回文子串。
public String longestPalindrome(String s) {
if (s == null || s.isEmpty())
return "";
String longestPalindrome = null;
int lpLen = 0;
for (int i = 0; i < s.length(); i++) {
for (int j = i + 1; j <= s.length(); j++) {
String subStr = s.substring(i, j);
if (isPalindrome(subStr) && subStr.length() > lpLen) {
longestPalindrome = subStr;
lpLen = subStr.length();
}
}
}
return longestPalindrome;
}
private boolean isPalindrome(String s) {
int sLen = s.length();
for (int i = 0; i < sLen / 2; i++) {
if (s.charAt(i) != s.charAt(sLen - i - 1)) {
return false;
}
}
return true;
}
时间复杂度为O(n³),空间复杂度为O(n)
解法二:中心扩展法
我最开始想到的就是这种方法。因为回文字符串左右是对称的,所以从中间字符向两侧查找,依次比较左右两个字符是否相等,如果相等就继续比较下一个字符,如果不等就停止。需要注意的是长度为奇数的回文串中间是单个字符,长度为偶数的回文串中间是两个相同的字符。
public String longestPalindrome(String s) {
if(s == null || s.isEmpty())
return "";
int startIndex=0,lastIndex=0;
for(int i=0; i<s.length(); i++) {
int lenOdd = catchPalindromeLen(s, i, i);
int lenEven = catchPalindromeLen(s, i, i+1);
int len = Math.max(lenOdd, lenEven);
if(len > (lastIndex+1 - startIndex)) {
startIndex = i-(len-1)/2;
lastIndex = i+len/2;
}
}
return s.substring(startIndex, lastIndex+1);
}
private int catchPalindromeLen(String s, int left, int right) {
int leftIndex = left,rightIndex = right;
while(leftIndex > -1 && rightIndex < s.length() && (s.charAt(leftIndex) == s.charAt(rightIndex))) {
leftIndex--;
rightIndex++;
}
return rightIndex - (leftIndex + 1);
}
时间复杂度为O(n²),空间复杂度为O(n)
解法三:动态规划法
这也是比较常见的一种方法,核心思想是以空间换时间,将已经计算过的结果保存起来,进行后续计算的时候不必从头开始重新计算,可以直接使用前面步骤中计算过的结果。对于长度为n的字符串s,先定义一个二维数组dp[n][n],dp[i][j]表示起始字符索引为i、结束字符索引为j的字符串是否是回文串,是的话值为1,不是值设为0。按照动态规划的思想,如果s[i] = s[j],并且dp[i+1][j-1]值为1的话,那么dp[i][j]=1。如果觉得公式不好理解,可以去看一下如何理解动态规划?,最高赞的讲解非常通俗易懂。
public String longestPalindrome(String s) {
if (s == null || s.isEmpty())
return "";
int len = s.length();
int maxNum = 1;
int startIndex = 0;
int[][] isPal = new int[len][len];
for (int i = 0; i < len; i++) {
isPal[i][i] = 1;
if (i < len - 1) {
if (s.charAt(i) == s.charAt(i + 1)) {
isPal[i][i + 1] = 1;
startIndex = i;
maxNum = 2;
}
}
}
for (int num = 3; num <= len; num++) {
for (int i = 0; i + num - 1 < len; i++) {
int j = num + i - 1;
if (s.charAt(i) == s.charAt(j) && isPal[i + 1][j - 1] == 1) {
isPal[i][j] = 1;
startIndex = i;
maxNum = num;
}
}
}
return s.substring(startIndex, startIndex + maxNum);
}
时间复杂度为O(n²),空间复杂度为O(n²)
解法四:Manacher算法
中文俗称马拉车算法。它可以将时间复杂度降到O(n)。具体讲解可以参考Manacher 算法。这个算法在中心扩展法的基础上,充分利用了回文串的对称性,这里截取关键部分如图所示:
设置两个变量,mx 和 id 。mx 代表以 id 为中心的最长回文的右边界,也就是mx = id + p[id]
。(其中p[id]
表示以 id 为中心的最长回文的半径)
假设我们现在求p[i]
,也就是以 i 为中心的最长回文半径,如果i < mx
,如上图,那么:
if (i < mx)
p[i] = min(p[2 * id - i], mx - i);
2 * id - i
为 i 关于 id 的对称点,即上图的 j 点,而**p[j]
表示以 j 为中心的最长回文半径**,因此我们可以利用p[j]
来加快查找。
public String longestPalindrome(String s) {
if (s == null || s.isEmpty()) {
return "";
}
//用#扩充字符串,开始结束分别添加^$
char[] es = extendStr(s);
int n = es.length;
int[] p = new int[n];
int ci = 0, ri = 0;//ci为中心字符索引,ri为中心字符的回文串最右侧字符索引
for (int i = 1; i < n - 1; i++) {//去除开始结束符,从索引为1的字符到索引为n-2的字符
int i_mirror = 2 * ci - i;
if (ri > i) {
p[i] = Math.min(ri - i, p[i_mirror]);
} else {
p[i] = 0;
}
while (es[i + 1 + p[i]] == es[i - 1 - p[i]]) {
p[i]++;
}
if (i + p[i] > ri) {
ci = i;
ri = p[i] + i;
}
}
int maxLen = 0;
int centerIndex = 0;
for (int i = 1; i < n - 1; i++) {
if (p[i] > maxLen) {
maxLen = p[i];
centerIndex = i;
}
}
int start = (centerIndex - maxLen) / 2;
return s.substring(start, start + maxLen);
}
private char[] extendStr(String s) {
int l = s.length();
char[] ec = new char[l * 2 + 3];//开始和结束分别添加^$,防止越界
ec[0] = '^';
int i = 1;
for (; i < (l * 2);) {
ec[i] = '#';
ec[i + 1] = s.charAt(i / 2);
i = i + 2;
}
ec[i] = '#';
ec[i + 1] = '$';
return ec;
}
该方法能理解就行,不需要一定可以手写出来。