問題描述
給定一個字符串
s
,找到s
中最長的迴文子串。你可以假設s
的最大長度爲 1000。
示例
輸入: "babad" 輸出: "bab" 注意: "aba" 也是一個有效答案。
問題分析
迴文串的意思是“從前往後讀和從後往前讀是一樣的”,舉個簡單的例子“上海自來水來自海上”。
所以這裏我們可以將原字符串反轉,求兩個字符串的最長公共子序列。
例如:abbc 的反轉 cbba,他們的最長公共子序列就是“bb”也就是所求。通過反轉之後求最長公共子序列來模擬從前往後和從後往前讀取的過程。
這裏需要特別注意的一點是 不是所有的公共子序列一定滿足條件。例如字符串 abcdba,它的反轉abdcba 和它的最長公共子序列爲"ab",但是顯然ab不是答案所求。所以我們在判斷的時候一定要檢查最長公共子序列在原字符串中的索引,是否對應的就是反轉後的那一塊索引
公共最長子序列法
public String longestPalindrome(String s) {
if (s.isEmpty() || s.length() == 1) {
return s;
}
char[] a = s.toCharArray();
int l = a.length;
int[][] temp = new int[l][l];
int max = 0, index = 0;
for (int i = 0; i < l; i++) {
for (int j = 0; j < l; j++) {
if (a[i] == a[l - j - 1]) {
if (i > 0 && j > 0) {
temp[i][j] = temp[i - 1][j - 1] + 1;
} else {
temp[i][j] = 1;
}
if (max < temp[i][j] && l - 1 - i == j - temp[i][j] + 1) {
max = temp[i][j];
index = i;
}
}
}
}
String result = s.substring(index + 1 - max, index + 1);
return result;
}
上述代碼我通過二維數組標記最長公共子序列的長度,這裏也可以改爲一維數組:
for (int i = 0; i < l; i++) {
for (int j = l - 1; j >= 0; j--) {
if (a[i] == a[l - j - 1]) {
if (i > 0 && j > 0) {
temp[j] = temp[j - 1] + 1;
} else {
temp[j] = 1;
}
if (max < temp[j] && l - 1 - i == j - temp[j] + 1) {
max = temp[j];
index = i;
}
} else {
temp[j] = 0;
}
}
}
其中 l - 1 - i == j - temp[i][j] + 1 這一步主要來判斷對應索引是否相等。其中 l 表示字符串長度,temp[i][j]表示當前計算出的最長公共子序列長。上述的判斷可以理解爲:
最長公共子序列在原字符串的結束索引是否和反轉字符串的開始索引相同
如果相同說明是同一塊字符串,即可以作爲迴文串計算,如果不相同說明是字符串內部相等,不能做爲迴文串計算。
問題分析
除了上述的通過反轉求最長公共子序列外,還可以通過暴力方法解決:遍歷所有可能出現迴文數的情況,加以判斷。
暴力法
public String longestPalindrome3(String s) {
if (s.isEmpty() || s.length() == 1) {
return s;
}
char[] temp = s.toCharArray();
int length = temp.length;
for (int i = length; i >= 2; i--) {
for (int j = 0; j <= length - i; j++) {
String ss = s.substring(j, i + j);
StringBuffer stringBuffer = new StringBuffer(ss);
if (ss.equals(stringBuffer.reverse().toString())) {
return stringBuffer.toString();
}
}
}
return temp[0] + "";
}
上述方法在letCode提交超時,可以參考代碼思路。 通過分析我們可以發現暴力法中多了很多沒必要的判斷。例如一個字符串S不是迴文串,那麼給它的左右分別加上不同的字符,那麼它肯定也不是迴文串。相反,如果一個字符串本身是迴文串,那麼給它的左右添加上相同的字符,它一定也是迴文串,而且我們知道,如果字符串的長度爲1,那麼它肯定是迴文串,如果它的長度爲2,如果兩個字符相等,那麼它也爲字符串。
通過上面的分析,假如我們用二維boolean數組boolean[i][j]表示從下標 i 到下標 j 是否迴文數,那麼滿足以下條件:
- 長度爲1: boolean[ i ][ i ] = true;
- 長度爲2: boolean[ i ][ i + 1] = temp[i] == temp[i+1]
- 長度>3: boolean[ i ][ j ] = boolean[ i + 1][ j - 1] && temp[i] == temp[j]
通過上面的分析,我們可以先初始化長度爲1和長度爲2,後續根據公式3依次往上計算,具體代碼如下:
動態規劃
public String longestPalindrome4(String s) {
if (s.isEmpty() || s.length() == 1) {
return s;
}
char[] temp = s.toCharArray();
int length = s.length();
boolean[][] jundge = new boolean[length + 1][length + 1];
int index = 0, end = 1;
for (int i = 1; i <= length; i++) {
jundge[i][i] = true;
if (i < length && temp[i - 1] == temp[i]) {
jundge[i][i + 1] = true;
index = i - 1;
end = i + 1;
}
}
for (int i = 3; i <= length; i++) {
for (int j = 1; j <= length - i + 1; j++) {
if (jundge[j + 1][j + i - 2] && temp[j - 1] == temp[j + i - 2]) {
jundge[j][j + i - 1] = true;
index = j - 1;
end = j + i - 1;
}
}
}
return s.substring(index, end);
}
上述代碼通過二維數組來標記之前的結果,通過觀察我們可以發現,判斷長度爲 i 的迴文串時,只和長度爲 i - 2 的迴文串有關,因此上述二維數組可以優化爲 jundge[length][2]。優化後的核心代碼如下:
boolean[][] jundge = new boolean[length + 1][2];
int index = 0, end = 1;
for (int i = 1; i <= length; i++) {
jundge[i][1] = true;
if (i < length && temp[i - 1] == temp[i]) {
jundge[i][0] = true;
index = i - 1;
end = i + 1;
}
}
for (int i = 3; i <= length; i++) {
for (int j = 1; j <= length - i + 1; j++) {
if (jundge[j + 1][i % 2] && temp[j - 1] == temp[j + i - 2]) {
jundge[j][i % 2] = true;
index = j - 1;
end = j + i - 1;
}else{
jundge[j][i % 2] = false;
}
}
}
除了上述通過dp和暴力的方法,還可以通過中心擴展算法解決該問題。因爲所有的迴文串都滿足中心對稱,因此我們遍歷所有節點,假設該節點就是最長迴文串的中心節點,其中節點和節點之前的空格也算,用它來標識長度爲偶數的迴文子串。那麼長度爲n的字符串,就有 (2 * n)/2 -1 箇中心節點,依次遍歷所有的中心節點,找出其中最長的迴文串即可。
中心擴展算法
public String longestPalindrome6(String s) {
if (s.isEmpty() || s.length() == 1) {
return s;
}
int index = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandLeftAndRight(s, i, i);
int len2 = expandLeftAndRight(s, i, i + 1);
int length = Math.max(len1, len2);
if (length > end - index) {
index = i - (length - 1) / 2;
end = i + length / 2;
}
}
return s.substring(index, end + 1);
}
public int expandLeftAndRight(String s, int left, int right) {
int index = left, end = right;
while (index >= 0 && end < s.length() && s.charAt(index) == s.charAt(end)) {
index--;
end++;
}
return end - index - 1;
}
上述算法中,len1表示以下標 i 爲對稱中心的情況,len2表示以下標 i 和 下標i+1 中心空格爲對稱中心的情況。每次取兩個中較大的一個進行計算即可。