兩個經典迴文字符串問題中的巧妙算法

問題一:(最長迴文子串)給定一個字符串 s,找到 s 中最長的迴文子串。
第一眼的想法是暴力法,由於其時間複雜度高達O(n^3),當s過長時效率會特別低。
方法一:中心擴展算法
其思想就是遍歷一遍字符串,其中在每一個點都進行以其爲中心而均勻展開(分奇偶),然後找到每個點能夠展開到的最大值,最後也就不難得到最長迴文串了。
具體代碼如下:

public String longestPalindrome1(String s) {
        if (s == null || s.length() < 1) return "";
        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);
            //通過start和end來保存最大len的兩端
            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;
    }

方法二:Manacher 算法
其思想就是將原字符串用同一的符號擴展兩倍+1,這樣可以巧妙的迴避了奇偶迴文串的問題。然後再定義一個類似計數排序的一個計數數組,用它在擴展後的字符串上進行類型中心擴展的操作。最終的Manacher 算法還有一個對計數數組的一個算法優化。
具體代碼如下:

public String longestPalindrome2(String s) {
        char[] ch = s.toCharArray();
        char[] ah = new char[2 * s.length() + 1];//迴避了要討論奇偶兩種情形的弊端
        for (int i = 0; i < 2 * s.length(); i = i + 2) {
            ah[i] = '#';
            ah[i + 1] = ch[i / 2];
        }
        ah[2 * s.length()] = '#';
        int[] arr = Manacher1(ah);
        return s.substring((arr[1] - arr[0]) / 2, (arr[1] + arr[0]) / 2);
    }

    private int[] Manacher1(char[] t) {
        int[] arr = new int[2];
        int[] len = new int[t.length];//既用於計數還能參與遍歷
        int ans = 0;
        for (int i = 0; i < len.length; i++) {
            len[i] = 1;
        }
        for (int i = 0; i < len.length; i++) {
            while ((i - len[i] >= 0) && (i + len[i] < len.length) && t[i - len[i]] == t[i + len[i]]) {
                len[i]++;
            }
            ans = Math.max(ans, len[i]);
        }
        arr[0] = ans - 1;
        int i = 0;
        while (len[i] != ans) {
            i++;
        }
        arr[1] = i;
        return arr;
    }
    private int[] Manacher2(char[] t) {
        int[] arr = new int[2];
        int[] len = new int[t.length];
        int ans = 0;
        //另外兩個指針起到優化算法的作用
        int mx = 0;
        int po = 0;
        for (int i = 0; i < len.length; i++) {
            if (mx > i) {
                len[i] = Math.min(mx - i, len[2 * po - i]);//如果前一個位置所形成的的迴文串長度很大,那麼下一個位置必定繼承其部分長度
            } else {
                len[i] = 1;
            }
            while ((i - len[i] >= 0) && (i + len[i] < len.length) && t[i - len[i]] == t[i + len[i]]) {
                len[i]++;
            }
            if (len[i] + i > mx) {
                mx = len[i] + i;
                po = i;
            }
            ans = Math.max(ans, len[i]);
        }
        arr[0] = ans - 1;
        int i = 0;
        while (len[i] != ans) {
            i++;
        }
        arr[1] = i;
        return arr;
    }

問題二:(最短迴文串)給定一個字符串 s,你可以通過在字符串前面添加字符將其轉換爲迴文串。找到並返回可以用這種方式轉換的最短迴文串。
思路:由於只能在字符串的前面添加字符,所以不由的想出要先找到原字符串的最長前綴迴文串,下面給出兩種方法。
方法一:直接尋找
這裏需要倒序遍歷字符串,定義一頭一尾兩個指針,再用一個while的內循環來取得其最長的前綴迴文串。
具體代碼如下:

public String shortestPalindrome1(String s) {
        if (s.equals("") || s.length() == 1) {
            return s;
        }
        int x = getFrontS(s);
        return new StringBuilder(s.substring(x + 1)).reverse().toString() + s;
    }
    //獲得原字符串的最長前綴迴文字符串長度
    private int getFrontS(String s) {
        int a = 0;
        for (int i = s.length() - 1; i > 0; i--) {
            if (s.charAt(a) == s.charAt(i)) {
                int b = i - 1;
                a++;
                while (a < b) {
                    if (s.charAt(a) == s.charAt(b)) {
                        a++;
                        b--;
                    }
                    else {
                        break;
                    }
                }
                if (a >= b) {
                    return i;
                }
                a = 0;
            }
        }
        return 0;
    }

方法二:KMP算法的next數組
這個next數組也很類似計數排序的那個計數數組,只不過這個數組記錄的是當前位置的最大前綴數,有了這個數組,最長前綴迴文串也就手到擒來了。
具體代碼如下:

public String shortestPalindrome2(String s) {
        if (s.equals("") || s.length() == 1) {
            return s;
        }
        String temp = s + "#" + new StringBuilder(s).reverse().toString() + "#";
        //中間那個#是爲了把兩段字符串分隔開,避免產生干擾
        int[] next = getNext1(temp);
        int index = next[temp.length()-1];  //取得最長前綴迴文字符串的下標
        return new StringBuilder(s.substring(index)).reverse().toString() + s;
    }
    private int[] getNext1(String p) {
        int[] next = new int[p.length() + 1];
        next[0] = -1;
        for (int i = 0; i < p.length(); i++) {
            next[i + 1] = next[i] + 1;
            while (next[i + 1] > 0 && p.charAt(next[i + 1] - 1) != p.charAt(i)) {
                next[i + 1] = next[next[i + 1] - 1] + 1;
            }
        }
        return next;
    }

特別是後面的Manacher 算法和KMP算法都很巧妙也很有回味價值。

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