5.最長回文子串-java實現

第五題:最長回文子串

給定一個字符串 s,找到 s 中最長的回文子串。你可以假設 s 的最大長度為 1000。
示例 1:

輸入: "babad"
輸出: "bab"
注意: "aba" 也是一個有效答案。

示例 2:

輸入: "cbbd"
輸出: "bb"

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/check-permutation-lcci
著作權歸領扣網絡所有。商業(yè)轉載請聯系官方授權,非商業(yè)轉載請注明出處。

V1版本

開始覺得這題和題3.無重復字符的最長子串一樣,甚至更簡單,以為只需要找到兩個最長的重復的字符,然后截取出中間的字符返回就行了
結果是一開始就誤解了其回文串的意思,導致修改提交了幾次都錯了
回文的意思是正著念和倒著念一樣,如:上海自來水來自海上
對應的最長回文串示例:

  "babad" ==> bab
  "" ==> ""
  "ac" ==> a
  "ccc" ==> ccc
  "abcda" ==> a

后續(xù)知道回文串的用意后,想到的是最長回文串其實最主要的還是要看是否有重復的字符,
然后校驗最是否滿足回文串的要求,如果滿足就直接截取字符串返回
沒有滿足的則按沒有滿足的退出
1.首先維護的是一個大頂堆,里面保存的是帶有重復的字符的起始位置數組
2.依次取出大頂堆中維護的數組,去校驗是否滿足回文串要求,對于相同長度的回文串,隨便返回哪個都行
代碼如下:

public String longestPalindrome(String s) {
        if (s.length() < 2) {
            return s;
        }
        // map用于維護 char字符與它出現過的下標位置
        Map<Character, List<Integer>> map = new HashMap<>();
        // 大頂堆
        PriorityQueue<int[]> maxHeap = new PriorityQueue<>(10, Comparator.comparingInt(o -> (o[0] - o[1])));

        // 臨時list
        List<Integer> list;
        for (int i = 0; i < s.length(); i++) {
            // 第一次出現,添加進map就退出
            if (!map.containsKey(s.charAt(i))) {
                list = new ArrayList<>(2);
                list.add(i);
                map.put(s.charAt(i), list);
                continue;
            }

            // 獲取歷史list
            list = map.get(s.charAt(i));
            for (Integer index : list) {
                // 有重復的情況,將出現的位置分別寫入到大頂推中
                maxHeap.add(new int[] {index, i});
            }
            list.add(i);
        }
        int[] poll;
        while (maxHeap.size() > 0) {
            poll = maxHeap.poll();
            if (checkLongestPalindrome(s, poll[0], poll[1])) {
                // 由于是前閉后開,end + 1
                return s.substring(poll[0], poll[1] + 1);
            }
        }
        return s.substring(0,1);
    }

    /**
     * 校驗是否滿足回文串要求
     */
    public boolean checkLongestPalindrome(String s, int start, int end) {
        while (start < end) {
            if (s.charAt(start) != s.charAt(end)) {
                return false;
            }
            start++;
            end--;
        }
        return true;
    }

知道這種方式運行效率會不高,空間用的也比較多,但沒想到會這么慘


image.png

V2版本

一直對這個回文串迷迷糊糊的,看了官方的解析頓時感覺豁然開朗
引用leecode原話:
對于一個子串而言,如果它是回文串,并且長度大于 22,那么將它首尾的兩個字母去除之后,它仍然是個回文串。
例如:
對于字符串 “ababa”,如果我們已經知道 “bab” 是回文串,那么 “ababa” 一定是回文串,這是因為它的首尾兩個字母都是“a”。

看到官方有三種方式

1.動態(tài)規(guī)劃
2.中心擴展算法
3.Manacher算法
文字太多,先按自己想到的來實現一下自己的V2版本,可能會和中心擴展的思路會比較像
每次循環(huán)都將 p(i - 1,i + 1) 與 p(i,i + 1)進行擴散,直至護展到不匹配為止,代碼如下

public String longestPalindrome(String s) {
        // 提前退出
        if (s.length() < 2) {
            return s;
        }
        int len = s.length();
        int start = 0;
        int maxLength = 0;
        // 臨時長度
        int length;
        // 臨時擴展次數
        int diffusionNum;
        for (int i = 0; i < len - 1; i++) {
            diffusionNum = getDiffusionNum(s, i - 1, i + 1);
            if (diffusionNum > 0) {
                length = diffusionNum * 2 + 1;
                if (length > maxLength) {
                    start = i - diffusionNum;
                    maxLength = length;
                }
            }

            diffusionNum = getDiffusionNum(s, i, i + 1);
            if (diffusionNum > 0) {
                length = diffusionNum * 2;
                if (length > maxLength) {
                    start = i - diffusionNum + 1;
                    maxLength = length;
                }
            }
        }
        maxLength = maxLength == 0? 1: maxLength;
        return s.substring(start, start + maxLength);
    }
    /**
     * 獲取擴展次數
     */
    private int getDiffusionNum(String s, int i, int j) {
        // 擴展次數
        int num = 0;
        while (i >= 0 && j < s.length() && s.charAt(i) == s.charAt(j)) {
            num++;
            i--;
            j++;
        }
        return num;
    }
image.png

雖然都有提升,但執(zhí)行時間還是挺久的,代碼的重復率也挺高的

V3版本

V3版本參考一官方的中心擴展法代碼
將兩種擴散的后的處理邏輯做了合并,獲了長度時還考慮了為負數的情況,很強大,后面遇到這種類型的問題不知道能不能靈活運用上,代碼如下

public String longestPalindrome(String s) {
        // 提前退出
        if (s.length() < 2) {
            return s;
        }
        int start = 0, end = 0, len;
        for (int i = 0; i < s.length(); i++) {
            len = Math.max(getLen(s, i, i), getLen(s, i, i+1));
            // 屏蔽了兩種實現的復雜度
            if (len > end - start) {
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        return s.substring(start, end + 1);
    }
    /**
     * 獲取長度
     */
    private int getLen(String s, int start, int end) {
        while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) {
            start--;
            end++;
        }
        // 還考慮了負數的情況
        return end - start - 1;
    }
image.png

依舊需要30幾ms,官文的幾個代碼都直接提交了一份,都需要幾十ms,在評論區(qū)一位大佬的代碼只需要4ms


image.png

執(zhí)行確認了一下,是個狠人


image.png

大佬的代碼先貼出來供大家參考
public String longestPalindrome(String s) {
        if (s == null || s.length() == 0) {
            return "";
        }
        // 保存起始位置,測試了用數組似乎能比全局變量稍快一點
        int[] range = new int[2];
        char[] str = s.toCharArray();
        for (int i = 0; i < s.length(); i++) {
            // 把回文看成中間的部分全是同一字符,左右部分相對稱
            // 找到下一個與當前字符不同的字符
            i = findLongest(str, i, range);
        }
        return s.substring(range[0], range[1] + 1);
    }

    public static int findLongest(char[] str, int low, int[] range) {
        // 查找中間部分
        int high = low;
        while (high < str.length - 1 && str[high + 1] == str[low]) {
            high++;
        }
        // 定位中間部分的最后一個字符
        int ans = high;
        // 從中間向左右擴散
        while (low > 0 && high < str.length - 1 && str[low - 1] == str[high + 1]) {
            low--;
            high++;
        }
        // 記錄最大長度
        if (high - low > range[1] - range[0]) {
            range[0] = low;
            range[1] = high;
        }
        return ans;
    }

大佬對這題的思路就更犀利了,因為上述的方式和官方的方式,步長都是p(i,i)及p(i,i+1)然后進行擴散,
這位大佬其實也是這種,不過他遇到連續(xù)重復的情況會將步長拉大
例如 abbbbbbbbbbbbbc這種字符串時
當解析到第一個b所在的位置時,傳統(tǒng)做法是就地擴散,這位大佬是直接將結束位置移動到最后一個b的位置,
因為相同的字符,無論奇數還是偶數個,都會是回文字串,且下次解析時,直接從最后一個b的位置開始解析,
如果遇到全一樣的字符的時候,只需要解析一次,時間復雜度為O(n),就尼瑪離譜

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容