給定一個字符串,輸出最長的迴文子串。迴文串指的是正的讀和反的讀是同樣的字符串,例如 "aba","ccbbcc"。java
暴力求解,列舉全部的子串,判斷是否爲迴文串,保存最長的迴文串。算法
public 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; } // 暴力解法 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; }
時間複雜度:兩層 for 循環 O(n²),for 循環裏邊判斷是否爲迴文,O(n),因此時間複雜度爲 O(n³)。c#
空間複雜度:O(1),常數個變量。segmentfault
根據迴文串的定義,正着和反着讀同樣,那咱們是否是把原來的字符串倒置了,而後找最長的公共子串就能夠了。例如,S = " caba",S' = " abac",最長公共子串是 "aba",因此原字符串的最長迴文串就是 "aba"。數組
關於求最長公共子串(不是公共子序列),有不少方法,這裏用動態規劃的方法,能夠先閱讀下邊的連接。函數
https://blog.csdn.net/u010397...優化
https://www.kancloud.cn/diges...spa
總體思想就是,申請一個二維的數組初始化爲 0,而後判斷對應的字符是否相等,相等的話.net
arr [ i ][ j ] = arr [ i - 1 ][ j - 1] + 1 。 3d
當 i = 0 或者 j = 0 的時候單獨分析,字符相等的話 arr [ i ][ j ] 就賦爲 1 。
arr [ i ][ j ] 保存的就是公共子串的長度。
public String longestPalindrome(String s) { if (s.equals("")) return ""; String origin = s; String reverse = new StringBuffer(s).reverse().toString(); //字符串倒置 int length = s.length(); int[][] arr = new int[length][length]; int maxLen = 0; int maxEnd = 0; for (int i = 0; i < length; i++) for (int j = 0; j < length; j++) { if (origin.charAt(i) == reverse.charAt(j)) { if (i == 0 || j == 0) { arr[i][j] = 1; } else { arr[i][j] = arr[i - 1][j - 1] + 1; } } if (arr[i][j] > maxLen) { maxLen = arr[i][j]; maxEnd = i; //以 i 位置結尾的字符 } } } return s.substring(maxEnd - maxLen + 1, maxEnd + 1); }
再看一個例子,S = "abc435cba",S’ = "abc534cba" ,最長公共子串是 "abc" 和 "cba" ,但很明顯這兩個字符串都不是迴文串。
因此咱們求出最長公共子串後,並不必定是迴文串,咱們還須要判斷該字符串倒置前的下標和當前的字符串下標是否是匹配。
好比 S = " caba ",S' = " abac " ,S’ 中 aba 的下標是 0 1 2 ,倒置前是 3 2 1,和 S 中 aba 的下標符合,因此 aba 就是咱們須要找的。固然咱們不須要每一個字符都判斷,咱們只須要判斷末尾字符就能夠。
首先 i ,j 始終指向子串的末尾字符。因此 j 指向的紅色的 a 倒置前的下標是 beforeRev = length - 1 - j = 4 - 1 - 2 = 1,對應的是字符串首位的下標,咱們還須要加上字符串的長度纔是末尾字符的下標,也就是 beforeRev + arr[ i ] [ j ] - 1 = 1 + 3 - 1 = 3,由於 arr[ i ] [ j ] 保存的就是當前子串的長度,也就是圖中的數字 3 。此時再和它與 i 比較,若是相等,則說明它是咱們要找的迴文串。
以前的 S = "abc435cba",S' = "abc534cba" ,能夠看一下圖示,爲何不符合。
當前 j 指向的 c ,倒置前的下標是 beforeRev = length - 1 - j = 9 - 1 - 2 = 6,對應的末尾下標是 beforeRev + arr[ i ] [ j ] - 1 = 6 + 3 - 1 = 8 ,而此時 i = 2 ,因此當前的子串不是迴文串。
代碼的話,在上邊的基礎上,保存 maxLen 前判斷一下下標匹不匹配就能夠了。
public String longestPalindrome(String s) { if (s.equals("")) return ""; String origin = s; String reverse = new StringBuffer(s).reverse().toString(); int length = s.length(); int[][] arr = new int[length][length]; int maxLen = 0; int maxEnd = 0; for (int i = 0; i < length; i++) for (int j = 0; j < length; j++) { if (origin.charAt(i) == reverse.charAt(j)) { if (i == 0 || j == 0) { arr[i][j] = 1; } else { arr[i][j] = arr[i - 1][j - 1] + 1; } } /**********修改的地方*******************/ if (arr[i][j] > maxLen) { int beforeRev = length - 1 - j; if (beforeRev + arr[i][j] - 1 == i) { //判斷下標是否對應 maxLen = arr[i][j]; maxEnd = i; } /*************************************/ } } return s.substring(maxEnd - maxLen + 1, maxEnd + 1); }
時間複雜度:兩層循環,O(n²)。
空間複雜度:一個二維數組,O(n²)。
空間複雜度其實能夠再優化一下。
咱們分析一下循環,i = 0 ,j = 0,1,2 ... 8 更新一列,而後 i = 1 ,再更新一列,而更新的時候咱們其實只須要上一列的信息,更新第 3 列的時候,第 1 列的信息是沒有用的。因此咱們只須要一個一維數組就能夠了。可是更新 arr [ i ] 的時候咱們須要 arr [ i - 1 ] 的信息,假設 a [ 3 ] = a [ 2 ] + 1,更新 a [ 4 ] 的時候, 咱們須要 a [ 3 ] 的信息,可是 a [ 3 ] 在以前已經被更新了,因此 j 不能從 0 到 8 ,應該倒過來,a [ 8 ] = a [ 7 ] + 1,a [ 7 ] = a [ 6 ] + 1 , 這樣更新 a [ 8 ] 的時候用 a [ 7 ] ,用完後纔去更新 a [ 7 ],保證了不會出錯。
public String longestPalindrome(String s) { if (s.equals("")) return ""; String origin = s; String reverse = new StringBuffer(s).reverse().toString(); int length = s.length(); int[] arr = new int[length]; int maxLen = 0; int maxEnd = 0; for (int i = 0; i < length; i++) /**************修改的地方***************************/ for (int j = length - 1; j >= 0; j--) { /**************************************************/ if (origin.charAt(i) == reverse.charAt(j)) { if (i == 0 || j == 0) { arr[j] = 1; } else { arr[j] = arr[j - 1] + 1; } /**************修改的地方***************************/ //以前二維數組,每次用的是不一樣的列,因此不用置 0 。 } else { arr[j] = 0; } /**************************************************/ if (arr[j] > maxLen) { int beforeRev = length - 1 - j; if (beforeRev + arr[j] - 1 == i) { maxLen = arr[j]; maxEnd = i; } } } return s.substring(maxEnd - maxLen + 1, maxEnd + 1); }
時間複雜度:O(n²)。
空間複雜度:降爲 O(n)。
解法一的暴力解法時間複雜度過高,在 leetCode 上並不能 AC 。咱們能夠考慮,去掉一些暴力解法中重複的判斷。咱們能夠基於下邊的發現,進行改進。
首先定義 P(i,j)。
$$P(i,j)=\begin{cases}true& \text{s[i,j]是迴文串} \\\\false& \text{s[i,j]不是迴文串}\end{cases}$$
接下來
$$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$
因此若是咱們想知道 P(i,j)的狀況,不須要調用判斷迴文串的函數了,只須要知道 P(i + 1,j - 1)的狀況就能夠了,這樣時間複雜度就少了 O(n)。所以咱們能夠用動態規劃的方法,空間換時間,把已經求出的 P(i,j)存儲起來。
若是 $$S[i+1,j-1]$$ 是迴文串,那麼只要 S [ i ] == S [ j ] ,就能夠肯定 S [ i , j ] 也是迴文串了。
求 長度爲 1 和長度爲 2 的 P ( i , j ) 時不能用上邊的公式,由於咱們代入公式後會遇到 $$P[i][j]$$ 中 i > j 的狀況,好比求 $$P[1][2]$$ 的話,咱們須要知道 $$P[1+1][2-1]=P[2][1]$$ ,而 $$P[2][1]$$ 表明着 $$S[2,1]$$ 是否是迴文串,顯然是不對的,因此咱們須要單獨判斷。
因此咱們先初始化長度是 1 的迴文串的 P [ i , j ],這樣利用上邊提出的公式 $$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$,而後兩邊向外各擴充一個字符,長度爲 3 的,爲 5 的,全部奇數長度的就都求出來了。
同理,初始化長度是 2 的迴文串 P [ i , i + 1 ],利用公式,長度爲 4 的,6 的全部偶數長度的就都求出來了。
public String longestPalindrome(String s) { int length = s.length(); boolean[][] P = new boolean[length][length]; int maxLen = 0; String maxPal = ""; for (int len = 1; len <= length; len++) //遍歷全部的長度 for (int start = 0; start < length; start++) { int end = start + len - 1; if (end >= length) //下標已經越界,結束本次循環 break; P[start][end] = (len == 1 || len == 2 || P[start + 1][end - 1]) && s.charAt(start) == s.charAt(end); //長度爲 1 和 2 的單獨判斷下 if (P[start][end] && len > maxLen) { maxPal = s.substring(start, end + 1); } } return maxPal; }
時間複雜度:兩層循環,O(n²)。
空間複雜度:用二維數組 P 保存每一個子串的狀況,O(n²)。
咱們分析下每次循環用到的 P(i,j),看一看能不能向解法二同樣優化一下空間複雜度。
當咱們求長度爲 6 和 5 的子串的狀況時,其實只用到了 4 , 3 長度的狀況,而長度爲 1 和 2 的子串狀況其實已經不須要了。可是因爲咱們並非用 P 數組的下標進行的循環,暫時沒有想到優化的方法。
以後看到了另外一種動態規劃的思路
https://leetcode.com/problems... 。
公式仍是這個不變
首先定義 P(i,j)。
$$P(i,j)=\begin{cases}true& \text{s[i,j]是迴文串}\\\\false& \text{s[i,j]不是迴文串}\end{cases}$$
接下來
$$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$
遞推公式中咱們能夠看到,咱們首先知道了 i +1 纔會知道 i ,因此咱們只須要倒着遍歷就好了。
public String longestPalindrome(String s) { int n = s.length(); String res = ""; boolean[][] dp = new boolean[n][n]; for (int i = n - 1; i >= 0; i--) { for (int j = i; j < n; j++) { dp[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1]); //j - i 表明長度減去 1 if (dp[i][j] && j - i + 1 > res.length()) { res = s.substring(i, j + 1); } } } return res; }
時間複雜度和空間複雜和以前都沒有變化,咱們來看看可不能夠優化空間複雜度。
當求第 i 行的時候咱們只須要第 i + 1 行的信息,而且 j 的話須要 j - 1 的信息,因此和以前同樣 j 也須要倒敘。
public String longestPalindrome7(String s) { int n = s.length(); String res = ""; boolean[] P = new boolean[n]; for (int i = n - 1; i >= 0; i--) { for (int j = n - 1; j >= i; j--) { P[j] = s.charAt(i) == s.charAt(j) && (j - i < 3 || P[j - 1]); if (P[j] && j - i + 1 > res.length()) { res = s.substring(i, j + 1); } } } return res; }
時間複雜度:不變,O(n²)。
空間複雜度:降爲 O(n ) 。
咱們知道迴文串必定是對稱的,因此咱們能夠每次循環選擇一箇中心,進行左右擴展,判斷左右字符是否相等便可。
因爲存在奇數的字符串和偶數的字符串,因此咱們須要從一個字符開始擴展,或者從兩個字符之間開始擴展,因此總共有 n + n - 1 箇中心。
public String longestPalindrome(String s) { if (s == null || s.length() < 1) return ""; int start = 0, end = 0; for (int i = 0; i < s.length(); i++) { int len1 = expandAroundCenter(s, i, i); int len2 = expandAroundCenter(s, i, i + 1); int len = Math.max(len1, len2); if (len > end - start) { start = i - (len - 1) / 2; end = i + len / 2; } } return s.substring(start, end + 1); } private int expandAroundCenter(String s, int left, int right) { int L = left, R = right; while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) { L--; R++; } return R - L - 1; }
時間複雜度:O(n²)。
空間複雜度:O(1)。
馬拉車算法 Manacher‘s Algorithm 是用來查找一個字符串的最長迴文子串的線性方法,由一個叫Manacher的人在1975年發明的,這個方法的最大貢獻是在於將時間複雜度提高到了線性。
主要參考了下邊連接進行講解。
https://segmentfault.com/a/11...
https://blog.crimx.com/2017/0...
http://ju.outofmemory.cn/entr...
https://articles.leetcode.com...
首先咱們解決下奇數和偶數的問題,在每一個字符間插入"#",而且爲了使得擴展的過程當中,到邊界後自動結束,在兩端分別插入 "^" 和 "$",兩個不可能在字符串中出現的字符,這樣中心擴展的時候,判斷兩端字符是否相等的時候,若是到了邊界就必定會不相等,從而出了循環。通過處理,字符串的長度永遠都是奇數了。
首先咱們用一個數組 P 保存從中心擴展的最大個數,而它恰好也是去掉 "#" 的原字符串的總長度。例以下圖中下標是 6 的地方。能夠看到 P[ 6 ] 等於 5,因此它是從左邊擴展 5 個字符,相應的右邊也是擴展 5 個字符,也就是 "#c#b#c#b#c#"。而去掉 # 恢復到原來的字符串,變成 "cbcbc",它的長度恰好也就是 5。
用 P 的下標 i 減去 P [ i ],再除以 2 ,就是原字符串的開頭下標了。
例如咱們找到 P[ i ] 的最大值爲 5 ,也就是迴文串的最大長度是 5 ,對應的下標是 6 ,因此原字符串的開頭下標是 (6 - 5 )/ 2 = 0 。因此咱們只須要返回原字符串的第 0 到 第 (5 - 1)位就能夠了。
接下來是算法的關鍵了,它充分利用了迴文串的對稱性。
咱們用 C 表示迴文串的中心,用 R 表示迴文串的右邊半徑。因此 R = C + P[ i ] 。C 和 R 所對應的迴文串是當前循環中 R 最靠右的迴文串。
讓咱們考慮求 P [ i ] 的時候,以下圖。
用 i_mirror 表示當前須要求的第 i 個字符關於 C 對應的下標。
咱們如今要求 P [ i ], 若是是用中心擴展法,那就向兩邊擴展比對就好了。可是咱們其實能夠利用迴文串 C 的對稱性。i 關於 C 的對稱點是 i_mirror ,P [ i_mirror ] = 3,因此 P [ i ] 也等於 3 。
可是有三種狀況將會形成直接賦值爲 P [ i_mirror ] 是不正確的,下邊一一討論。
當咱們要求 P [ i ] 的時候,P [ mirror ] = 7,而此時 P [ i ] 並不等於 7 ,爲何呢,由於咱們從 i 開始日後數 7 個,等於 22 ,已經超過了最右的 R ,此時不能利用對稱性了,但咱們必定能夠擴展到 R 的,因此 P [ i ] 至少等於 R - i = 20 - 15 = 5,會不會更大呢,咱們只須要比較 T [ R+1 ] 和 T [ R+1 ]關於 i 的對稱點就好了,就像中心擴展法同樣一個個擴展。
此時P [ i_mirror ] = 1,可是 P [ i ] 賦值成 1 是不正確的,出現這種狀況的緣由是 P [ i_mirror ] 在擴展的時候首先是 "#" == "#" ,以後遇到了 "^"和另外一個字符比較,也就是到了邊界,才終止循環的。而 P [ i ] 並無遇到邊界,因此咱們能夠繼續經過中心擴展法一步一步向兩邊擴展就好了。
此時咱們先把 P [ i ] 賦值爲 0 ,而後經過中心擴展法一步一步擴展就好了。
就這樣一步一步的求出每一個 P [ i ],當求出的 P [ i ] 的右邊界大於當前的 R 時,咱們就須要更新 C 和 R 爲當前的迴文串了。由於咱們必須保證 i 在 R 裏面,因此一旦有更右邊的 R 就要更新 R。
此時的 P [ i ] 求出來將會是 3 ,P [ i ] 對應的右邊界將是 10 + 3 = 13,因此大於當前的 R ,咱們須要把 C 更新成 i 的值,也就是 10 ,R 更新成 13。繼續下邊的循環。
public String preProcess(String s) { int n = s.length(); if (n == 0) { return "^$"; } String ret = "^"; for (int i = 0; i < n; i++) ret += "#" + s.charAt(i); ret += "#$"; return ret; } // 馬拉車算法 public String longestPalindrome2(String s) { String T = preProcess(s); int n = T.length(); int[] P = new int[n]; int C = 0, R = 0; for (int i = 1; i < n - 1; i++) { int i_mirror = 2 * C - i; if (R > i) { P[i] = Math.min(R - i, P[i_mirror]);// 防止超出 R } else { P[i] = 0;// 等於 R 的狀況 } // 碰到以前講的三種狀況時候,須要利用中心擴展法 while (T.charAt(i + 1 + P[i]) == T.charAt(i - 1 - P[i])) { P[i]++; } // 判斷是否須要更新 R if (i + P[i] > R) { C = i; R = i + P[i]; } } // 找出 P 的最大值 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); }
時間複雜度:for 循環裏邊套了一層 while 循環,難道不是 O ( n² )?不!實際上是 O ( n )。不嚴謹的想一下,由於 while 循環訪問 R 右邊的數字用來擴展,也就是那些還未求出的節點,而後不斷擴展,而期間訪問的節點下次就不會再進入 while 了,能夠利用對稱獲得本身的解,因此每一個節點訪問都是常數次,因此是 O ( n )。
空間複雜度:O(n)。
時間複雜度從三次方降到了一次,美妙!這裏兩次用到了動態規劃去求解,初步認識了動態規劃,就是將以前求的值保存起來,方便後邊的計算,使得一些多餘的計算消失了。而且在動態規劃中,經過觀察數組的利用狀況,從而下降了空間複雜度。而 Manacher 算法對迴文串對稱性的充分利用,不得不讓人歎服,本身加油啦!