【算法】Longest Palindromic Substring

最近刷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ō)下思路:

  1. 原字符間插入特殊字符(#),處理長(zhǎng)度為偶數(shù)情況;
  2. 循環(huán)遍歷每個(gè)字符算出其最大回文子串的長(zhǎng)度;
  3. 再剔除得到的回文子串中的特殊字符(#);

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;
  1. 變量含義:i 和 iMirror 表示字符串中兩個(gè)字符索引,centerIndex 是其對(duì)稱(chēng)點(diǎn)索引。p[i]表示以i索引的字符為中心的左右對(duì)稱(chēng)字符對(duì)數(shù)。rightIndex 則是以centerIndex點(diǎn)字符的最大回文子串的最右邊索引位置。
  2. 由于對(duì)稱(chēng)性,iMirror 為 i 的對(duì)稱(chēng)點(diǎn)不難得出 iMirror = centerIndex - (i - centerIndex),即代碼中的 int iMirror = 2 * centerIndex - i;
  3. 當(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)查閱參考文章。

參考

其它

說(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ā)博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容