徒手挖地球三週目

徒手挖地球三週目

NO.5 最長迴文子串 中等

Q8ZSII.png

思路一:暴力法 用兩個for循環劃分出所有子串,並依次判斷劃分出的子串是否爲迴文,如果是迴文並且子串長度大於ans當前記錄的值,就更新ans。

public String longestPalindrome(String s) {
    String ans="";
    int len=0;
    for (int i=0;i<s.length();i++){
        for (int j=i+1;j<s.length();j++){
            if (isPalindrome(s.substring(i,j))&&len<j-i+1){
                ans=s.substring(i,j);
                len=Math.max(len,ans.length());
            }
        }
    }
    return ans;
}

boolean isPalindrome(String s){
    for (int i=0;i<s.length()/2;i++){
        if (s.charAt(i)!=s.charAt(s.length()-1-i))
            return false;
    }
    return true;
}

時間複雜度:O(n^3)

思路二:擴展中心法 經過對迴文特點的觀察發現,迴文都是中心對稱的。所以我們可以從中心進行展開判斷,一個長度爲n的字符串中有2n-1箇中心(因爲迴文長度有可能是基數或偶數,基數迴文的中心有n個,如abc中心是b;偶數迴文的中心有n-1個,如abbc中心是bb)。

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++){
//            判斷i是否爲奇數長度迴文串的中心
            int len1=expandAroundCenter(s,i,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;
            }
        }
//        因爲substring()方法截取的範圍是[起始索引,結束索引),所以第二個參數需要+1
        return s.substring(start,end+1);
    }

    public int expandAroundCenter(String s,int left,int right){
//        當左標記大於等於0,且右標記小於輸入串長,且當前左右標記的字符相等時,左右標記分別中心擴展
        while (left>=0&&right<s.length()&&s.charAt(left)==s.charAt(right)){
            left--;
            right++;
        }
//        返回以i或(i,i+1)爲中心的迴文串長度。
        return right-left-1;
    }

時間複雜度:O(n^2)

思路三:最長公共子串法(LCS) 迴文是從左向右讀和從右向左讀都是一樣的,所以我們可以將原字符串s倒置之後獲得s’,然後取s和s’的最長公共子串ans作爲最長迴文子串。

用動態規劃法求最長公共子串,大概思路是:1.申請一個二維數組arr[s.length][s’.length]。2.判斷每個對應位置的字符是否相等,如果相等 arr[i][j]=arr[i-1][j-1]+1;當i=0或j=0時候單獨分析,如果對應位置字符相等 arr[i][j]=1。(**PS:**arr[i][j]保存的就是公共子串的長度。)

public String longestPalindrome(String s) {
        if (s.equals(""))return "";
        String origin=s;
//        倒置原字符串
        String reverse=new StringBuffer(s).reverse().toString();
//        maxLen記錄最長公共子序列,maxEnd記錄最長公共子序列的結尾下標
        int maxLen=0,maxEnd=0;
//        分別以原字符串長度和倒置字符串長度來表示,是爲了更直觀的理解該二維數組
        int[][] arr=new int[origin.length()][reverse.length()];
//        雙重循環遍歷二維數組
        for (int i=0;i<origin.length();i++){
            for (int j=0;j<reverse.length();j++){
//                判斷原字符串i位置字符和倒置字符串j位置字符是否相等
                if (origin.charAt(i)==reverse.charAt(j)){
                    if (i==0||j==0){//當i=0或j=0時候單獨分析,如果對應位置字符相等 arr[i][j]=1
                        arr[i][j]=1;
                    }else{
                        arr[i][j]=arr[i-1][j-1]+1;
                    }
                }
//                如果當前公共子串長度比maxLen所記錄的值更大,則更新最長公共子串的長度及其結束下標
                if (arr[i][j]>maxLen){
                    maxLen=arr[i][j];
                    maxEnd=i;
                }
            }
        }
        return s.substring(maxEnd-maxLen+1,maxEnd+1);
    }

當S=“abc435cba”,S’="abc534cba"時,上述算法依然可以計算出最長公共子串"abc"來作爲最長迴文子串,這顯然是不對的。對於這個問題的解決思路是:1.因爲j一直指向倒置字符串中子串的末尾字符,可以先求出j指向的字符X倒置之前的下標beforeReverse=length-1-j。2.此時求出的beforeReverse是X在原字符串中的子串首位的下標,還需要加上當前子串的長度纔是原字符串中子串末尾的下標e,即e=beforeReverse+arr[i][j]-1。3.因爲i一直指向原字符串中子串的末尾字符,所以將e與i進行比較,如果相等,則說明當前找到的公共子串是迴文子串。

例如,字符串倒置前後分別是S=“abc435cba”,S’=“abc534cba”,當i=2且j=2時,arr[2][2]=3,然後進行計算出beforeReverse=length-1-j=9-1-2=6,判斷beforeReverse+arr[2][2]-1是否等於i,顯然 6+3-1!=2,所以當前子串不是迴文子串且不需要更新maxLen和maxEnd。

Qamn76.png

針對該思路,只需要在更新maxLen和maxEnd之前添加下標是否匹配的判斷即可:

public String longestPalindrome(String s) {
        if (s.equals(""))return "";
        String origin=s;
//        倒置原字符串
        String reverse=new StringBuffer(s).reverse().toString();
//        maxLen記錄最長公共子序列,maxEnd記錄最長公共子序列的結尾下標
        int maxLen=0,maxEnd=0;
//        分別以原字符串長度和倒置字符串長度來表示,是爲了更直觀的理解該二維數組
        int[][] arr=new int[origin.length()][reverse.length()];
//        雙重循環遍歷二維數組
        for (int i=0;i<origin.length();i++){
            for (int j=0;j<reverse.length();j++){
//                判斷原字符串i位置字符和倒置字符串j位置字符是否相等
                if (origin.charAt(i)==reverse.charAt(j)){
                    if (i==0||j==0){//當i=0或j=0時候單獨分析,如果對應位置字符相等 arr[i][j]=1
                        arr[i][j]=1;
                    }else{
                        arr[i][j]=arr[i-1][j-1]+1;
                    }
                }
//                如果當前公共子串長度比maxLen所記錄的值更大,則更新最長公共子串的長度及其結束下標
                if (arr[i][j]>maxLen){
                    int beforeReverse=origin.length()-1-j;
//					  添加下標是否匹配的判斷                 
                    if (beforeReverse+arr[i][j]-1==i) {
                        maxLen = arr[i][j];
                        maxEnd = i;
                    }
                }
            }
        }
        return s.substring(maxEnd-maxLen+1,maxEnd+1);
    }

時間複雜度:O(n^2)

寫到這裏,利用LCS算法解決求最長迴文子串的問題已經基本完成了,經過查閱資料和學習之後發現:其實可以使用一個一位數組即可,而不必使用上述的二維數組arr[][]。空間複雜度從之前的用二維數組時的O(n^2)降到了用一維數組後的O(n)。

例如還是上面的那個數組S=“abc435cba”,i=0,j=1、2、3、4、5、6、7、8更新了第一列;i=2j=1、2、3、4、5、6、7、8更新了第二列,以此類推直到i=8且j=8每一列都更新完畢。但是經過觀察發現,每次更新時只需要參考前一列的值,更新第三列時,第一列的值就用不到了,所以只需要一個一維數組就可以了。但是,更新arr[i]的時候需要arr[i-1]的值,例如arr[3]=arr[2]+1,arr[4]=arr[3]+1,此時的arr[3]的信息已經被更新過了並不是”之前一列的信息了“,所以循環時j不能從0到8遞增,應該倒過來,arr[8]=arr[7]+1、arr[7]=arr[6]+1。。。更新arr[8]時用arr[7],用完之後才能去更新arr[7]:

public String longestPalindrome(String s) {
        if (s.equals(""))return "";
        String origin=s;
//        倒置原字符串
        String reverse=new StringBuffer(s).reverse().toString();
//        maxLen記錄最長公共子序列,maxEnd記錄最長公共子序列的結尾下標
        int maxLen=0,maxEnd=0;
//        分別以原字符串長度和倒置字符串長度來表示,是爲了更直觀的理解該二維數組
        int[] arr=new int[s.length()];
//        雙重循環遍歷二維數組
        for (int i=0;i<origin.length();i++){
            for (int j=reverse.length()-1;j>=0;j--){
//                判斷原字符串i位置字符和倒置字符串j位置字符是否相等
                if (origin.charAt(i)==reverse.charAt(j)){
                    if (i==0||j==0){//當i=0或j=0時候單獨分析,如果對應位置字符相等 arr[j]=1
                        arr[j]=1;
                    }else{
                        arr[j]=arr[j-1]+1;
                    }
                }else {//之前是二維數組每一列默認值就是0,現在是一維數組所以需要手動更新爲0
                    arr[j]=0;
                }
//                如果當前公共子串長度比maxLen所記錄的值更大,則更新最長公共子串的長度及其結束下標
                if (arr[j]>maxLen){
                    int beforeReverse=origin.length()-1-j;
                    if (beforeReverse+arr[j]-1==i) {
                        maxLen = arr[j];
                        maxEnd = i;
                    }
                }
            }
        }
        return s.substring(maxEnd-maxLen+1,maxEnd+1);
    }

思路四:Manacher算法 在擴展中心算法中,將奇數長度迴文子串和偶數長度的迴文子串分別進行了處理。本算法首先解決了奇數和偶數的問題,在每個字符間插入“#”,並且爲了使得擴展的過程中,到邊界後自動結束,在兩端分別插入“^”和“$”,這樣重心擴展的時候,判斷兩端字符是否相等時,如果到了邊界就一定不會相等,從而結束循環(這裏的“#”“^”“$”是字符串中不存在的字符)。並且,經過插入特殊字符處理後,字符串的長度永遠都是奇數了。

例如,
	"aba" 擴展爲 "^#a#b#a#$"
	"acca" 擴展爲 "^#a#c#c#a#$"
	"cbcbccde" 擴展爲 "^#c#b#c#b#c#c#d#e#$"

字符串擴展之後,我們申請一個數組p[]保存從中心擴展的最大個數,而這個數也剛好是去掉”#“之後原子串的長度。例如下圖中下標爲6的字符,p[6]=5,所以它是從左邊擴展5個字符,相應的右邊也是擴展5個字符,也就是“#c#b#c#b#c#”。而去掉“#”恢復到原來的子串,變成“cbcbc”,它的長度剛好也是5。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-OjETCtOA-1576226380243)(https://s2.ax1x.com/2019/12/08/QawkCQ.png)]

求原字符串下標:用p的下標i減去p[i],再除以2,就是原字符串的開頭下標了。例如,我們找到上圖中p[i]最大值爲5,也就是迴文串的最大長度是5,對應的下標是6,所以原子串在原字符串中的開頭下標是(6-5)/2=0。所以我們只需要返回原字符串的第0到第(5-1)位就可以了。

既然已經知道了如何利用p[]數組巧妙地取得結果子串了,那麼就要進行馬拉車算法最重要的步驟了,即如何求p[]數組?

這一步是馬拉車算法的精髓所在,充分利用的迴文的對稱性。用c表示迴文子串的中心,用r表示迴文子串的右邊半徑。所以r=c+p[i]。C 和 R 所對應的迴文串是當前循環中 R 最靠右的迴文串,而不一定是最長的迴文串。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-f4LMttbw-1576226380244)(https://s2.ax1x.com/2019/12/09/Q0m8Ld.png)]

用 i_mirror 表示當前需要求的第 i 個字符關於 C 對應的下標。 我們現在要求 P [ i ],如果是用中心擴展法,那就向兩邊擴展比對就行了。但是我們其實可以利用迴文串中心 C 的對稱性。i 關於 C 的對稱點是 i_mirror,P [ i_mirror ] = 3,所以 P [ i ] 也等於 3。

但是有三種情況將會造成直接賦值爲 P [ i_mirror ] 是不正確的,下邊一一討論:

情況一:超出了 R

Q0mJeA.png

當我們要求 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 ] 遇到了原字符串的左邊界

Q0mtot.png

此時P [ i_mirror ] = 1,但是 P [ i ] 賦值成 1 是不正確的,出現這種情況的原因是 P [ i_mirror ] 在擴展的時候首先是 “#” == “#”,之後遇到了 “^” 和另一個字符比較,也就是到了邊界,才終止循環的。而 P [ i ] 並沒有遇到邊界,所以我們可以繼續通過中心擴展法一步一步向兩邊擴展就行了。

情況三:i 等於了 R

此時我們先把 P [ i ] 賦值爲 0,然後通過中心擴展法一步一步擴展就行了。

考慮 C 和 R 的更新
就這樣一步一步的求出每個 P [ i ],當求出的 P [ i ] 的右邊界大於當前的 R 時,我們就需要更新 C 和 R 爲當前的迴文串了。因爲我們必須保證 i 在 R 裏面,所以一旦有更右邊的 R 就要更新 R。

Q0mYdI.png

此時的 P [ i ] 求出來將會是 3,P [ i ] 對應的右邊界將是 10 + 3 = 13,所以大於當前的 R,我們需要把 C 更新成 i 的值,也就是 10,R 更新成 13。繼續下邊的循環。

public String longestPalindrome(String s) {
//        獲取擴充後的字符串T
        String T=preProsess(s);
        int len=T.length();
        int[] p=new int[len];
        int c=0,r=0;
//        不需要判斷前後邊界字符“^"和“$”,所以循環範圍是[1,len-1)
        for (int i=1;i<len-1;i++){
//            第i個字符關於c對稱的下標
            int i_mirror=2*c-i;
            if (r>i){//如果i小於對稱半徑r
                p[i]=Math.max(r-i,p[i_mirror]);
            }else {
                p[i]=0;
            }

//            遇到三種特殊情況時,需要退化到中心擴展法
            while (T.charAt(i+1+p[i])==T.charAt(i-1-p[i])){
                p[i]++;
            }

//            判斷是否需要更新c和r
            if (i+p[i]>r){
                c=i;
                r=p[i];
            }
        }
//        找出p[]數組中最大的值
        int currentIndex=0,maxLen=0;
        for (int i=0;i<p.length;i++){
            if (p[i]>maxLen){
                currentIndex=i;
                maxLen=p[i];
            }
        }

//        求子串首字符在原字符串中的下標
        int start=(currentIndex-maxLen)/2;
        return s.substring(start,start+maxLen);
    }
//    擴充字符串
    public String preProsess(String s){
        if (s.equals(""))return "$";
        String result="^";
        for (int i=0;i<s.length();i++){
            result+="#"+s.charAt(i);
        }
        result+="#$";
        return result;
    }

時間複雜度:O(n)


第四題查找兩個有序數組的中位數,由於我實在垃圾不能完全理解算法的精妙之處就暫緩對其的學習和記錄。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章