两个经典回文字符串问题中的巧妙算法

问题一:(最长回文子串)给定一个字符串 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算法都很巧妙也很有回味价值。

你可能感兴趣的:(编程算法思想)