通俗易懂的最長迴文串圖解、說明及Java程式碼(中心擴散法和Manacher演算法)

原來的1024發表於2020-12-17

1. 迴文串

作為程式設計師,迴文串這個詞已經見怪不怪了,就是一個字串正著讀和反著讀是一樣的,形式如abcdcba、bbaabb。這裡涉及到奇迴文偶迴文,奇迴文指回文串的字元數是奇數,偶迴文指回文串的字元數是偶數。前面舉的abcdcba就是奇迴文,bbaabb就是偶迴文。判斷一個字串是否是迴文串很簡單,只要從字串的兩端開始往中間掃描,全部匹配成功則是迴文串,只要有一次匹配失敗,那麼就不是迴文串。程式碼如下

// 沒有對字串為null或者空串的返回值進行考慮
static boolean Palindrome(String s){
    for(int i = 0, j = s.length()-1; i < j; i++, j--){
        if(s.charAt(i) != s.charAt(j)){
            return false;
        }
    }
    return true;
}

 

2. 最長迴文串

在我們瞭解迴文串內容後,如果給你一個字串,你能不能得到該字串中的最長迴文串呢?

2.1 暴力匹配法

最長迴文串簡單的解法就是暴力匹配法,依次判斷所有字元數大於1個的子串是否迴文串,並記錄最長的那個迴文串。如acbc字串,得到字元數大於1的子串ac、cb、bc;acb、cbc;acbc,其中cbc是最長迴文串。雖然暴力匹配法思路清晰、程式碼簡單,但是如果字串長度較長時,那麼子串的數量是很龐大的,對於一個長度為n的字串,它的子串有n(n-1)/2個,加上判斷子串是否為迴文串的時間複雜度是O(n),所以最終總的時間複雜度是O(n^3)左右。暴力匹配留給大家自行編寫程式碼,博主就偷個懶不寫了。

2.2 中心擴散法

中心擴散法是另一種迴文串解決方法,演算法思路是從字串的第一個字元一直遍歷到最後一個字元,每次從該字元往兩邊掃描,如果左右兩邊的值相等,那麼往左右兩邊擴充,直至左右兩邊的值不相等或者越界,掃描結束,記錄此時的左右邊界下標,並且記錄此時的迴文串長度。該方法的時間消耗主要是遍歷字串的每個字元,以及每個字元需要向兩邊擴充擴散,所以總的時間複雜度為O(n^2)

圖解:以下以abcfcbd字串遍歷到 f 字元進行圖解,如下圖。

1. 當遍歷abcfcbd字串的 f 字元時,先令left和right都指向 f 字元。

2. 往左右擴充,可以擴充,left往左移,right往右移

3. 可以擴充,繼續移動

4. 不可以繼續擴充,結束,記錄left和right的位置

 

程式碼

 public String longestPalindrome(String s) {
     int len = s.length();
     if(len <= 1){
         return s;
     }        
     
     int max = 0;
     int[] index = new int[2];
     for(int i = 0; i < len-1; i++){

         // 考慮奇數迴文還是偶數迴文,所以分別計算以i為中心,以i和i+1為中心兩種方式的迴文串
         int[] f1 = findSub(s, i, i);
         int[] f2 = findSub(s, i, i+1);
         int f1Len = f1[1] - f1[0];
         int f2Len = f2[1] - f2[0];

         // 如果以i為中心的奇迴文串長度更長並且大於前面記錄的最大回文串長度max,更新max
         // 如果以i和i+1為中心的偶迴文串長度更長並且大於前面記錄的最大回文串長度max,更新max
         if((f1Len > f2Len) && (f1Len > max)){
             index[0] = f1[0];
             index[1] = f1[1];
             max = f1Len;
         }else if((f1Len <= f2Len) && (f2Len > max)){
             index[0] = f2[0];
             index[1] = f2[1];
             max = f2Len;
         }
     }
     return s.substring(index[0], index[1]+1);
 }

 static int[] findSub(String s, int left, int right){
     // 如果是偶數迴文,left和right不等,需要判斷一下left和right的值是否相等
     if(s.charAt(left) != s.charAt(right)){
         return new int[]{left+1, left+1};
     }
     while((left >= 0) && (right <= s.length()-1) && (s.charAt(left) == s.charAt(right))){
         left--;
         right++;
     }
     return new int[]{left+1, right-1};
 }

 

2.3 Manacher演算法 

Manacher演算法是一種以O(n)時間複雜度得到最長迴文串的演算法,以該演算法的發明者Manacher老先生名字命名。雖然該演算法的解釋網上較多,但是有點繁瑣和難懂,博主儘量以自己小白的理解力詳細地進行說明。我們接下來先說說Manacher演算法的主要思想,它到底在哪裡進行了優化?然後我們再上程式碼。接下來我們以dcbcdcbca字串為例,請耐心閱讀

2.3.1. 對字串dcbcdcbca先預處理。

在每個字元兩旁插入分割符,可以是任意字元,因為博主一開始也覺得分隔符不能是字串中出現的字元,那這裡選取'a'字元作為分割符進行證明,預處理後得到如下字串str2

2.3.2. 記錄每個字元的迴文半徑

遍歷每個字元時,將每個字元可以向左右兩邊擴充的長度稱為迴文半徑,使用val陣列記錄迴文半徑。則str2的第1個字元到第13個字元迴文半徑資料值如下圖所示。

2.3.3 Manacher演算法的優化之處

其實計算str2的第1個字元到第13個字元迴文半徑時Manacher也有優化,只是接下來更好講解,所以現在分析。

當掃描到str2的第10個字元d時,此時的迴文字串是acabacadacabaca,如下圖所示。

接下來我們要計算str2的第14個字元 b,正常情況下,我們以b為中心向兩邊擴充;Manacher演算法的強大就是在此處進行了優化。

因為b處在axis和right之間,我們可以看看str2第14個b字元關於axis對稱的第6個b字元它的迴文半徑是多少,為什麼可以這樣呢?

接下來看圖解吧,原本以為自己理解了很好描述,但現在發現自己理解而已,要想描述清楚還是有點難,大家看看圖解吧!

1. 步驟1

 

2. 步驟2

 

3. 步驟3

 

總結:Manacher演算法進行優化的部分主要有兩點:①字串預處理,新增分割符;②利用迴文串的對稱資訊,避免重複計算迴文半徑。

看來這種演算法還是有些難描述的,大家見諒,還是隻能多花點時間去消化,Manacher演算法最重要一點就是利用對稱資訊

 

程式碼

public String longestPalindrome(String s) {
        int len = s.length();
        int newLen = 2 * len + 1;
        // 字串預處理,得到填充分隔符後的字元陣列
        char[] newStr = new char[newLen];
        for(int i = 0; i < len; i++){
            newStr[2*i] = 'a';
            newStr[2*i+1] = s.charAt(i);
        }
        newStr[newLen-1] = 'a';

        // ans是最長迴文串的迴文半徑,ansIndex是最長迴文串的對稱中心
        int[] val = new int[newLen];
        int axis = 0;
        int right = 0;
        int ans = 0;
        int ansIndex = 0;
        for(int i = 0; i < newLen; i++){
            // 如果當前遍歷字元處於迴文串的最遠邊界內,那麼可以利用對稱資訊
            if(i < right){
                val[i] = Math.min(val[2*axis-i], right-i+1);
            }else{
                val[i] = 1;
            }
            // 沒有越界,並且迴文串向左右擴充成功,那麼迴文半徑加1
            while(i-val[i] >= 0 && i+val[i] < newLen && newStr[i-val[i]] == newStr[i+val[i]]){
                val[i]++;
            }
            // 如果當前遍歷字元的邊界大於記錄的最遠邊界,更新迴文串的最遠邊界
            if(i+val[i]-1 > right){
                right = i+val[i]-1;
                axis = i;
            }
            // 記錄最長迴文串的迴文半徑和對稱中心
            if(val[i] > ans){
                ans = val[i];
                ansIndex = i;
            }
        }
        
        StringBuilder sb = new StringBuilder();
        for(int i = ansIndex-ans+1; i < ansIndex+ans-1; i++){
            sb.append(newStr[++i]);
        }
        return sb.toString();
    }

 

以下是力扣的執行結果

相關文章