No.005 Longest Palindromic Substring
5. Longest Palindromic Substring
- Total Accepted: 120226
- Total Submissions: 509522
- Difficulty: Medium
Given a string s, find the longest palindromic(迴文) substring in sS. You may assume that the maximum length of s is 1000, and there exists one unique longest palindromic substring.
推薦解法3,直觀,有效,好理解
方法一:暴力搜尋(O(N³))
這個思路就很簡單了,就是直接求出每一個子串,然後判斷其是否為迴文。我們從子串長度最長開始,依次遞減,如果遇到是迴文的,則直接返回即可。迴圈結束如果沒有迴文,則返回null。
值得注意的一點是因為題目直接說了肯定存在迴文,並且最大長度的迴文唯一,所以在當字串長度大於1的時候,最大回文長度必定大於1(如果為1的話,則每一個單獨字元都可以作為最長長度的迴文),所以搜尋長度遞減到2就結束了。題目中肯定存在大於2的迴文,所以不會直到最後迴圈結束返回null這一步,所以最後直接寫的返回null無關緊要。
1 /* 2 * 方法一:暴力搜尋 3 */ 4 public String longestPalindrome(String s) { 5 if (s == null || s.length() == 0 || s.length() == 1) { 6 return s; 7 } 8 9 String sub; 10 for (int subLen = s.length(); subLen > 1; subLen--) { 11 for (int startIndex = 0; startIndex <= (s.length() - subLen); startIndex++) { 12 // 列出所有子串,然後判斷子串是否滿足有重複 13 if (startIndex != (s.length() - subLen)) { 14 sub = s.substring(startIndex, startIndex + subLen); 15 } else { 16 sub = s.substring(startIndex); 17 } 18 System.out.println(sub); 19 if (isPalindrome(sub)) { 20 return sub; 21 } 22 } 23 } 24 25 return null ; 27 } 28 29 private boolean isPalindrome(String sub) { 30 for(int i = 0 ; i <= sub.length()/2 ; i++){ 31 if(sub.charAt(i) != sub.charAt(sub.length()-i-1)){ 32 return false ; 33 } 34 } 35 return true ; 36 }
方法二:動態規劃O(N²)
更簡潔的做法,使用動態規劃,這樣可以把時間複雜度降到O(N²),空間複雜度也為O(N²)。做法如下:
首先,寫出動態轉移方程。
Define P[ i, j ] ← true iff the substring Si … Sj is a palindrome, otherwise false.
P[ i, j ] ← ( P[ i+1, j-1 ] and Si = Sj ) ,顯然,如果一個子串是迴文串,並且如果從它的左右兩側分別向外擴充套件的一位也相等,那麼這個子串就可以從左右兩側分別向外擴充套件一位。
其中的base case是
P[ i, i ] ← true P[ i, i+1 ] ← ( Si = Si+1 )
然後,看一個例子。
假設有個字串是adade,現在要找到其中的最長迴文子串。使用上面的動態轉移方程,有如下的過程:
按照紅箭頭->黃箭頭->藍箭頭->綠箭頭->橙箭頭的順序依次填入矩陣,通過這個矩陣記錄從i到j是否是一個迴文串。
1 /*
2 * 方法二:動態規劃的方法
3 */
4 public String longestPalindrome2(String s) {
5 if (s == null || s.length() == 0 || s.length() == 1) {
6 return s;
7 }
8 char [] arr = s.toCharArray() ;
9 int len = s.length() ;
10 int startIndex = 0 ;
11 int endIndex = 0 ;
12 boolean [][] dp = new boolean [len][len] ;
13 dp[0][0] = true ;
14 for(int i = 1 ; i < len ; i++){
15 //dp[i][i]置為true
16 dp[i][i] = true ;
17 //dp[i-1][i]判斷true或false
18 if(arr[i-1] != arr[i]){
19 dp[i-1][i] = false ;
20 }else{
21 dp[i-1][i] = true ;
22 startIndex = i-1 ;
23 endIndex = i ;
24 }
25 }
26 //填充其他地方的值
27 for(int l = 2 ; l < len ; l++){
28 for(int i = 0 ; i < len-l ; i++){
29 int j = i+l ;
30 if(dp[i+1][j-1] && (arr[i] == arr[j])){
31 dp[i][j] = true ;
32 if((j-i) > (endIndex - startIndex)){
33 startIndex = i ;
34 endIndex = j ;
35 }
36 }
37 }
38 }
39 //返回最長迴文字串
40 if(endIndex == (len-1)){
41 return s.substring(startIndex) ;
42 }else{
43 return s.substring(startIndex, endIndex+1) ;
44 }
45 }
下面的方法參考自 http://blog.csdn.net/feliciafay/article/details/16984031
方法三:從中間向兩邊展開O(N²)(比動態規劃方法好理解)
迴文字串顯然有個特徵是沿著中心那個字元軸對稱。比如aha沿著中間的h軸對稱,a沿著中間的a軸對稱。那麼aa呢?沿著中間的空字元''軸對稱。所以對於長度為奇數的迴文字串,它沿著中心字元軸對稱,對於長度為偶數的迴文字串,它沿著中心的空字元軸對稱。對於長度為N的候選字串,我們需要在每一個可能的中心點進行檢測以判斷是否構成迴文字串,這樣的中心點一共有2N-1個(2N-1=N-1 + N)。檢測的具體辦法是,從中心開始向兩端展開,觀察兩端的字元是否相同。程式碼如下:
1 //從中間向兩邊展開
2 string expandAroundCenter(string s, int c1, int c2) {
3 int l = c1, r = c2;
4 int n = s.length();
5 while (l >= 0 && r <= n-1 && s[l] == s[r]) {
6 l--;
7 r++;
8 }
9 return s.substr(l+1, r-l-1);
10 }
11
12 string longestPalindromeSimple(string s) {
13 int n = s.length();
14 if (n == 0) return "";
15 string longest = s.substr(0, 1); // a single char itself is a palindrome
16 for (int i = 0; i < n-1; i++) {
17 string p1 = expandAroundCenter(s, i, i); //長度為奇數的候選迴文字串
18 if (p1.length() > longest.length())
19 longest = p1;
20
21 string p2 = expandAroundCenter(s, i, i+1);//長度為偶數的候選迴文字串
22 if (p2.length() > longest.length())
23 longest = p2;
24 }
25 return longest;
26 }
四、 時間複雜度為O(N)的演算法
在這裡看到了更更簡潔的做法,可以把時間複雜度降到O(N).具體做法原文說得很清楚,有圖有例,可以仔細讀讀。這裡我只想寫寫,為什麼這個演算法的時間複雜度是O(N)而不是O(N²)。從程式碼中看,for迴圈中還有個while,在2層巢狀的迴圈中,似乎應該是O(N²)的時間複雜度。
1 // Transform S into T.
2 // For example, S = "abba", T = "^#a#b#b#a#$".
3 // ^ and $ signs are sentinels appended to each end to avoid bounds checking
4 string preProcess(string s) {
5 int n = s.length();
6 if (n == 0) return "^$";
7 string ret = "^";
8 for (int i = 0; i < n; i++)
9 ret += "#" + s.substr(i, 1);
10
11 ret += "#$";
12 return ret;
13 }
14
15 string longestPalindrome(string s) {
16 string T = preProcess(s);
17 int n = T.length();
18 int *P = new int[n];
19 int C = 0, R = 0;
20 for (int i = 1; i < n-1; i++) {
21 int i_mirror = 2*C-i; // equals to i' = C - (i-C)
22
23 P[i] = (R > i) ? min(R-i, P[i_mirror]) : 0;
24
25 // Attempt to expand palindrome centered at i
26 while (T[i + 1 + P[i]] == T[i - 1 - P[i]])
27 P[i]++;
28
29 // If palindrome centered at i expand past R,
30 // adjust center based on expanded palindrome.
31 if (i + P[i] > R) {
32 C = i;
33 R = i + P[i];
34 }
35 }
36
37 // Find the maximum element in P.
38 int maxLen = 0;
39 int centerIndex = 0;
40 for (int i = 1; i < n-1; i++) {
41 if (P[i] > maxLen) {
42 maxLen = P[i];
43 centerIndex = i;
44 }
45 }
46 delete[] P;
47
48 return s.substr((centerIndex - 1 - maxLen)/2, maxLen);
49 }
時間複雜度為什麼是O(N)而不是O(N²)呢?
假設真的是O(N²),那麼在每次外層的for迴圈進行的時候(一共n步),對於for的每一步,內層的while迴圈要進行O(N)次。而這是不可能。因為p[i]和R是有相互影響的。while要麼就只走一步,就到了退出條件了。要麼就走很多很步。如果while走了很多步,多到一定程度,會更新R的值,使得R的值增大。而一旦R變大了,下一次進行for迴圈的時候,while條件直接就退出了。