Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
Example:
Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.
Example:
Input: "cbbd"
Output: "bb"
大致意思就是給定一個字符串s,找到s中最長回文子串。
回文的含義是:正著看和倒著看相同,如abba和abcba.
子串的含義是:在原串中連續(xù)出現(xiàn)的字符串片段。
這是一道經(jīng)典的算法題,在《算法競賽入門經(jīng)典》以及2016騰訊實習(xí)生校招筆試題中都曾遇到過,之前的文章中也曾解決過一個更復(fù)雜的問題。
解決該問題最方便的方法是中心擴(kuò)展法,從中心向兩邊擴(kuò)展回文串,需要注意的是,回文串有兩種形式:長度為奇數(shù)的回文串(形如abcba)以及長度為偶數(shù)的回文串(形如abba).
使用這種方法尋找最長回文子串的時間復(fù)雜度為O(n?^2??),空間復(fù)雜度為O(1)。
代碼如下:
public class Solution {
public String longestPalindrome(String s) {
int n = s.length();
int start = 0, maxLen = 0;
for (int i = 0; i < n; i++) { // 以i為中心向兩邊擴(kuò)展
for (int j = 0; i - j >= 0 && i + j < n; j++) { // 長度為奇數(shù)的回文串(形如abcba)
if (s.charAt(i - j) != s.charAt(i + j)) {
break;
}
if (2 * j + 1 > maxLen) {
maxLen = 2 * j + 1;
start = i - j;
}
}
for (int j = 0; i - j >= 0 && i + 1 + j < n; j++) { // 長度為偶數(shù)的回文串(形如abba),中心點(兩個b)位置分別為i和i+1,向兩邊擴(kuò)展范圍(i-j,i+1+j)
if (s.charAt(i - j) != s.charAt(i + 1 + j)) {
break;
}
if (2 * j + 2 > maxLen) {
maxLen = 2 * j + 2;
start = i - j;
}
}
}
return s.substring(start, start + maxLen);
}
}
此外,解決該問題還可以使用Manacher算法(中文名馬拉車算法),它可以在O(N)的時間復(fù)雜度內(nèi)得到一個字符串中以每一個字符為中心的最長回文子串。而對于長度為偶數(shù)的回文串,比如abba
,可以通過插入未出現(xiàn)過的特殊字符#,從而轉(zhuǎn)化成長度為奇數(shù)的回文串#a#b#b#a#
該算法的基本原理就是利用已知回文串的左半部分來推導(dǎo)右半部分。
用數(shù)組p[i]表示以i為中心的最長回文子串半徑長度,從前向后掃描字符串s中的每一個字符計算出它們對應(yīng)的p[i],找到最大的p[i]即找到了s的最長回文子串。
在從前向后掃描的過程中,需要計算p[i]時一定已經(jīng)計算好了p[1]....p[i-1],假設(shè)現(xiàn)在掃描到了i+k這個位置,需要計算p[i+k]
定義max是以i+k之前的字符為中心的所有回文串所能延伸到的最遠(yuǎn)的位置(回文右邊界),假設(shè)這個字符的位置是i,即max = i + p[i];// i為i+k之前到達(dá)最遠(yuǎn)的回文串中心
這時i+k的位置分布有兩種情況:
i+k這個位置不在前面的任何回文串內(nèi)部(不含max點),即 i+k >= max,這時初始化
p[i+k] = 0;//本身是回文串
然后p[i+k]左右延伸,即while(s.charAt(i+k - p[i+k]) == s.charAt(i+k + p[i+k])) p[i+k]++;
i+k這個位置被前面以i為中心的回文串包含,即 i+k < max,這樣的話p[i+k]就不是從0開始了
對于第二種情況,根據(jù)回文串的性質(zhì),可知i+k這個位置的字符關(guān)于i與i-k對稱,這時p[i+k]又要分為以下三種情況來計算(黑色是以i為中心的回文串范圍,綠色是以i-k為中心的回文串范圍)
2.1如上圖,若以i-k為中心的回文串有一部分在以i為中心的回文串范圍之外(綠色的左端在黑色范圍之外),則以i+k為中心的回文串范圍一定是橙色部分,有 p[i+k] = max - (i+k) = i + p[i] - i - k = p[i] - k,即 p[i+k] = Math.min(p[i-k], p[i]-k);
(p[i] - k < p[i-k])
如果小于橙色部分,則違反了與i-k關(guān)于i的對稱性
假設(shè)超出橙色部分,如圖中的紫色延長線c和d,根據(jù)以i為中心的回文子串性質(zhì),b部分與c部分相對于i對稱;同理根據(jù)以i-k為中心的回文子串性質(zhì),b部分與a部分相對于i-k對稱,最終得出a部分與d部分相對于i位置對稱,超出了以i為中心的回文子串(黑色)范圍,假設(shè)不成立。
如上圖,若以i-k為中心的回文串全部在以i為中心的回文串范圍內(nèi)且未達(dá)到左側(cè)邊界(左側(cè)綠色在黑色范圍之內(nèi)),則以i+k為中心的回文串范圍一定是右側(cè)綠色部分,有p[i+k] = p[i-k],即 p[i+k] = Math.min(p[i-k], p[i]-k);
(p[i-k] < p[i] - k)
如果小于右側(cè)綠色部分,同樣是違反了與i-k關(guān)于i的對稱性
假設(shè)超出右側(cè)綠色部分,如圖中的紫色延長線c和d,根據(jù)以i為中心的回文子串性質(zhì),b部分與c部分相對于i對稱,a部分與d部分也相對于i對稱,最終得出a部分與b部分相對于i-k位置對稱,超出了以i-k為中心的回文子串(左側(cè)綠色)范圍,假設(shè)不成立。
如上圖,若以i-k為中心的回文串全部在以i為中心的回文串范圍內(nèi)且恰好達(dá)到左側(cè)邊界(左側(cè)綠色到達(dá)黑色范圍左邊界),則有 i-k - p[i-k] = i - p[i],即 p[i-k] = p[i] - k
以i+k為中心的回文串范圍一定包含右側(cè)綠色部分,并有可能超出(如圖橙色部分),這時初始化 p[i+k] = Math.min(p[i-k], p[i]-k);
(p[i-k] = p[i] - k),并向外延伸while(s.charAt(i+k - p[i+k]) == s.charAt(i+k + p[i+k])) p[i+k]++;
綜合上面所有情況,總結(jié)出核心代碼:
p[i+k] = 0;
if(i+k < max) {
p[i+k] = Math.min(p[i-k], p[i]-k);
}
while(s.charAt(i+k - p[i+k]) == s.charAt(i+k + p[i+k])) {
p[i+k]++;
}
上面分析中把當(dāng)前的字符定義在位置i+k,配合i及i-k這種對稱的命名方式是為了便于我們理解,在實際編程中當(dāng)前字符的位置通常為i,最長延伸回文串的中心點通常定義成id(id < i),則i關(guān)于id的對稱位置為id - (i - id) = 2 * id - i,把上面的思想帶入到實際編程中:
在遍歷到位置為i的字符時,已知以位置為id的字符為中心的回文串已延伸到的最遠(yuǎn)位置max = id + p[id],如果當(dāng)前字符在這個回文串中,我們就要賦值 p[i] = Math.min(p[2 * id - i], max - i)
,即以當(dāng)前位置i關(guān)于id的對稱位置2 * id - i為中心的回文串半徑與該對稱位置距離最長延展回文串左邊界長度max - i二者間的較小值。代碼如下:
public class Solution {
public String longestPalindrome(String s) {
String t = "#";
for (int i = 0; i < s.length(); i++) { // 插入#,統(tǒng)一轉(zhuǎn)化為長度為奇數(shù)的回文串
t += s.charAt(i) + "#";
}
int[] p = new int[t.length()]; // p[i]表示以i為中心的最長回文子串半徑長度
int maxP = 0, maxC = 0; // 數(shù)組p中的最大值,最長回文子串的中心點
int id = 0; // 當(dāng)前位置之前已到達(dá)最遠(yuǎn)的回文串中心
int max = id + p[id]; // 當(dāng)前查找位置之前,已知能影響最右邊的串
for (int i = 0; i < t.length(); i++) {
if (i < max) { // 當(dāng)前字符在之前最遠(yuǎn)回文串的范圍之內(nèi)
p[i] = Math.min(p[2 * id - i], max - i);
}
while (i - p[i] >= 0 && i + p[i] < t.length() && t.charAt(i - p[i]) == t.charAt(i + p[i])) {
p[i]++;
}
if (i + p[i] > max) { // 更新最遠(yuǎn)回文串
id = i;
max = id + p[id];
}
if (p[i] > maxP) { // 保留最大值信息
maxP = p[i];
maxC = i;
}
}
if (t.charAt(maxC) == '#') { // 還原長度為偶數(shù)的回文串
return s.substring((maxC - 2) / 2 - (maxP - 2) / 2, (maxC - 2) / 2 + 1 + (maxP - 2) / 2 + 1);
} else { // 還原長度為奇數(shù)的回文串
return s.substring((maxC - 1) / 2 - (maxP - 1) / 2, (maxC - 1) / 2 + (maxP - 1) / 2 + 1);
}
}
}
總體來說,Manacher算法還是比較復(fù)雜的,不會有人要求你在45分鐘的coding session中去實現(xiàn)它,大致了解即可。