最近刷LeetCode遇到一個(gè)比較有意思的題目(Longest Palindromic Substring),求一個(gè)字符串的最大回文子串。題目本身并不難,但需要理清思路才好理解,借此文記錄下。
題目
Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
給定一個(gè)字符串,找出其最長(zhǎng)的回文子串。你可假定字符串長(zhǎng)度最大不超過(guò)1000。
什么是最長(zhǎng)回文子串?
一個(gè)正反順序打印輸出結(jié)果相等的字符串。左右字符具備對(duì)稱(chēng)性。
舉例: “aabbaa" 、 "bbcbb"、"bccb" 等等。
怎么處理字符串長(zhǎng)度奇偶情況
- 各字符間插入特殊字符。比如插入:#
- 從相鄰兩個(gè)字符開(kāi)始比較。
我的解法
大概說(shuō)下思路:
- 原字符間插入特殊字符(#),處理長(zhǎng)度為偶數(shù)情況;
- 循環(huán)遍歷每個(gè)字符算出其最大回文子串的長(zhǎng)度;
- 再剔除得到的回文子串中的特殊字符(#);
Java 代碼實(shí)現(xiàn)
public String longestPalindrome(String s) {
if (s == null || s.length() <= 1) {
return s;
}
//插入 # 解決偶數(shù)對(duì)稱(chēng)字符
s = insertSpecialChar(s, '#');
final int length = s.length();
// 最大回文子串長(zhǎng)度
int maxLen = 0;
int[] longestIndex = new int[2];
int i = 0, j = 0;
for (int index = 1; index < length; index++) {
i = index - 1;
j = index + 1;
while ((i >= 0 && j <= length - 1) && (s.charAt(i) == s.charAt(j))) {
i--;
j++;
}
if (maxLen < (j - i - 1)) {
maxLen = j - i - 1;
longestIndex[0] = i + 1;
longestIndex[1] = j;//substring 方法 endIndex 可以等于length 。取值范圍是[startIndex, endIndex)
}
}
final String longestPalindromeStr = s.substring(longestIndex[0], longestIndex[1]);
return deleteSpecialChar(longestPalindromeStr, '#'));
}
// 插入特殊字符
private String insertSpecialChar(String s, char specialChar) {
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
sBuilder.append(specialChar + String.valueOf(s.charAt(i)));
}
sBuilder.append(specialChar);
return sBuilder.toString();
}
//剔除
private String deleteSpecialChar(String s, char specialChar) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (specialChar != s.charAt(i)) {
stringBuilder.append(String.valueOf(s.charAt(i)));
}
}
return stringBuilder.toString();
}
這個(gè)解法思路比較簡(jiǎn)單,按照官方說(shuō)法這就是蠻力解決方案(brute force solution)。效率缺陷就在于檢查后面字符的回文子串時(shí)有可能前面已經(jīng)比較過(guò)的字符又得重復(fù)比較一次。
官方推薦解法1
public String longestPalindrome(String s) {
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;
}
思路還是比較清晰,但未使用字符間插入特殊字符,而循環(huán)比較i和i 、i和i+1(也就是處理最大回文字符長(zhǎng)度為偶數(shù)的情況,相鄰的兩個(gè)數(shù)相等)。較上一種解法少了插入和剔除特殊字符的操作,但每次遍歷都多一次i和i+1的比較,按照正常理解如果字符串太長(zhǎng),此方法應(yīng)該會(huì)耗時(shí)更多才對(duì);于是我做了個(gè)測(cè)試,同樣的字符串(長(zhǎng)度約為1w),實(shí)際測(cè)試下來(lái),此解法較上一種執(zhí)行反而更快。我猜原因應(yīng)該是:插入和剔除特殊字符操作耗時(shí)、插入后字符長(zhǎng)度翻倍、比較邏輯簡(jiǎn)單三個(gè)原因?qū)е隆S衅渌?jiàn)解的同學(xué)請(qǐng)留言解惑,謝謝。
官方推薦解法2
public String longestPalindromeOfficialSn(String s) {
if (s == null || s.length() <= 1) {
return s;
}
//插入 # 解決偶數(shù)對(duì)稱(chēng)字符
final String temp = insertSpecialChar(s, '#');
final int length = temp.length();
int centerIndex = 0, rightIndex = 0;
final int[] p = new int[length];
for (int i = 0; i < length; i++) {
// iMirror 為 i 的對(duì)稱(chēng)點(diǎn)
int iMirror = 2 * centerIndex - i;
int pMirror = (iMirror < 0) ? 0 : p[iMirror];
p[i] = (rightIndex > i) ? Math.min(pMirror, rightIndex - i) : 0;
int L = i - 1 - p[i];
int R = i + 1 + p[i];
while (L >= 0 && R < length && temp.charAt(L) == temp.charAt(R)) {
p[i]++;
L = i - 1 - p[i];
R = i + 1 + p[i];
}
if (i + p[i] > rightIndex) {
// 右移已算出的回文子串
rightIndex = i + p[i];
centerIndex = i;
}
}
// 找出最大回文子串: 數(shù)組中最大的數(shù)
int maxLength = 0, index = 0;
for (int j = 0; j < length; j++) {
if (maxLength < p[j]) {
maxLength = p[j];
index = j;
}
}
String ret = temp.substring(index - maxLength, index + maxLength);
System.out.println( "index = "+index+" maxLength "+maxLength);
return s.substring((index - maxLength)/2, (index + maxLength)/2);
}
此解法最大優(yōu)勢(shì)就是利用回文子串的對(duì)稱(chēng)性,避免重復(fù)比較,提高執(zhí)行效率;也省去了剔除特殊字符的過(guò)程。
要想理解此解法關(guān)鍵要看懂這兩句代碼
int iMirror = 2 * centerIndex - i;
p[i] = (rightIndex > i) ? Math.min(pMirror, rightIndex - i) : 0;
- 變量含義:i 和 iMirror 表示字符串中兩個(gè)字符索引,centerIndex 是其對(duì)稱(chēng)點(diǎn)索引。p[i]表示以i索引的字符為中心的左右對(duì)稱(chēng)字符對(duì)數(shù)。rightIndex 則是以centerIndex點(diǎn)字符的最大回文子串的最右邊索引位置。
- 由于對(duì)稱(chēng)性,iMirror 為 i 的對(duì)稱(chēng)點(diǎn)不難得出 iMirror = centerIndex - (i - centerIndex),即代碼中的 int iMirror = 2 * centerIndex - i;
- 當(dāng)前點(diǎn)i + 對(duì)稱(chēng)點(diǎn)iMirror的p[iMirror] 如果小于rightIndex則可得出p[i] = p[iMirror],看下面代碼:
if(i + p[iMirror] < rightIndex){
p[i] = p[iMirror];
}else{
p[iMirror] >= rightIndex - i;
//根據(jù)p[centerIndex]的回文子串對(duì)稱(chēng)性可知,p[i]>=rightIndex - i; 超過(guò)rightIndex為未知情況,所以去最小值p[i] = rightIndex - i;
}
// 所以就得出了下面這句代碼。其中i>=rightIndex的情況未比較過(guò)的字符,所以默認(rèn)賦值0
p[i] = (rightIndex > i) ? Math.min(pMirror, rightIndex - i) : 0;
此算法重點(diǎn)在于理解對(duì)稱(chēng)性,避免重復(fù)比較。欲知詳情請(qǐng)查閱參考文章。
參考
- 附官方解題鏈接,具體思路分析及復(fù)雜度均有詳解。
- 最好的中文解析文章
- 最長(zhǎng)連續(xù)回文串
其它
說(shuō)下文章標(biāo)題中的 DSAA ,其實(shí)就是數(shù)據(jù)結(jié)構(gòu)與算法(Data Structures And Algorithms)的英文字母縮寫(xiě),這樣命名主要是想寫(xiě)一個(gè)系列文章來(lái)分享和記錄我的算法學(xué)習(xí)過(guò)程,與君共勉。代碼托管在github,歡迎star。
本文為原創(chuàng)內(nèi)容,轉(zhuǎn)載請(qǐng)說(shuō)明出處,首發(fā)博客。