小題大做-最長迴文子串

寫在前面

李小龍的妻子琳達在《我眼中的布魯斯》回憶裏寫道,她問丈夫:“作爲世界第一,是不是不畏懼所有的對手?”。

李小龍否認:“我不是世界第一,我也有害怕的對手。”

妻子聽到十分驚訝,追問:“什麼樣的對手讓你害怕?”

李小龍說“我不怕會一萬種招式的人,我只怕把一種招式練了一萬遍的對手。”

對於算法題,也是如此,我們追求的不是把一種題解出來,而是找到這道題所有的解法,所以推出小題大做系列,意在深度剖析算法題,做到將一種招式練一萬遍。

希望可以通過這個系列的博客,鍛鍊自己使用多種方式解決遇到的問題的能力。

題幹

給定一個字符串s,找到s中最長的迴文子串。可以假設s的最大長度爲1000。

迴文串是從左向右讀和從右向左讀一樣的字符串。
子串是原始字符串的連續子集,子序列是原始字符串的一個子集,這裏要求是子串。

解法

暴力解法

雙重for循環遍歷所有可能,start和end記錄起始與結束位置,最後截取字符串(截取字符串較爲耗性能,所以前期使用指針記錄,最後截取)。
但是這個方法時間複雜度較高(O(n^3)),js暴力解法在Leetcode是無法通過的。

var longestPalindrome = function (s) {
    let len = s.length;
    if (len < 2) return s;
    let maxLen = 1;
    let start = 0; // 記錄最長迴文子串的起始位置
    let end = 0; // 記錄最長迴文子串的結束位置
    // 雙重for循環,遍歷所有情況
    for (let i = 0; i < len; i++) {
        for (let j = i; j < len; j++) {
            if (isPalindrome(i, j)) {
                let tempLen = j - i + 1;
                if (tempLen > maxLen) {
                    maxLen = tempLen;
                    start = i;
                    end = j;
                }
            }
        }
    }
    return s.slice(start, end + 1)
    // 判斷是否爲迴文
    function isPalindrome(i, j) {
        while (i < j) {
            if (s.charAt(i) != s.charAt(j)) {
                return false
            }
            i ++;
            j --;
        }
        return true
    }
};

中心擴散法

枚舉最長迴文子串的中心可能出現的所有位置,然後向外擴散,考慮中心是一個字符的情況,也考慮中心是兩個字符的情況。
中心是兩個字符的情況時,左指針要在右邊,否則,即使while循環一次也沒進,判斷出來的最長迴文子串也有兩個字符。

時間複雜度O(n^2)
可以通過LeetCode

var longestPalindrome = function (s) {
    let len = s.length;
    if (len < 2) return s;
    let maxLen = 0; // 記錄最長迴文子串長度
    let start = 0; // 記錄起始位置
    let end = 0; // 記錄結束位置
    for (let i = 1; i < len; i++) {
        let tempSartEnd = findMaxPalindrome(i);
        let tempLen = tempSartEnd[1] - tempSartEnd[0] + 1
        if (tempLen > maxLen) {
            maxLen = tempLen
            start = tempSartEnd[0]
            end = tempSartEnd[1]
        }
    }
    return s.slice(start, end + 1)
    function findMaxPalindrome(i) {
        // 中心爲一個字符的情況
        let left = i;
        let right = i;
        // 中心爲兩個字符的情況(這時候,left_指針要在右邊,因爲這時候及時while循環一次沒走,判斷出來的最長迴文也是兩個字符)
        let left_ = i - 1;
        let right_ = i;
        // 如果迴文子串的中心是一個字符
        while (s.charAt(left - 1) && s.charAt(right + 1) && s.charAt(left - 1) == s.charAt(right + 1)) {
            left--;
            right++;
        }
        // 如果迴文子串的中心是兩個字符
        while (s.charAt(left_ - 1) && s.charAt(right_ + 1) && s.charAt(left_ - 1) == s.charAt(right_ + 1)) {
            left_--;
            right_++;
        }
        // 返回較長的
        return (right_ - left_) > (right - left) ? [left_, right_] : [left, right]
    }
};

動態規劃法

要想判斷第start到end位爲迴文字符串,需要判斷start + 1到end - 1位是不是迴文字符串。
我們可以列出一個表格(二維數組,初始全部爲null)

起始\結束 0 1 2 3 4
0
1
2
3
4

當起始位置與結束位置相同時,一定是迴文,所以我們初始化表格的時候將start==end的位置設置爲true

起始\結束 0 1 2 3 4
0
1
2
3
4

由於start必定小於等於end,所以我們只需要求表格右上部分即可。
由於想要求start到end是否迴文需要依賴start+1到end-1是否迴文,所以求當前格子需要依賴左下方格子
就有一下幾種情況

  1. 左下爲null,則比較當前start與end是否相等,相等則此格子爲true,不等爲false。
  2. 左下爲true,則比較當前start與end是否相等,相等則此格子爲true,不等爲false。
  3. 左下爲false,此格子爲false。

由於需要依賴左下方格子,所以填表順序如下

起始\結束 0 1 2 3 4
0 1 2 4 7
1 3 5 8
2 6 9
3 10
4

如果發現新的迴文字符串,則與之前最大的迴文字符串組比較,取較大的一個。

var longestPalindrome = function (s) {
    let len = s.length;
    if (len < 2) return s;
    let maxLen = 0; // 記錄最長迴文子串長度
    let start = 0; // 記錄起始位置
    let end = 0; // 記錄結束位置
    let map = new Array(len);
    // 初始化表格
    for (let i = 0; i < len; i++) {
        map[i] = new Array(len).fill(null);
        map[i][i] = true
    }
    // 填表
    for (let j = 1; j < len; j++) {
        for (let i = 0; i < j; i++) {
            if (map[i + 1][j - 1] == true || map[i + 1][j - 1] == null) {
                if (s.charAt(i) == s.charAt(j)) {
                    map[i][j] = true;
                    let tempMaxLen = j - i + 1;
                    if (tempMaxLen > maxLen) {
                        maxLen = tempMaxLen;
                        start = i;
                        end = j;
                    }
                } else {
                    map[i][j] = false;
                }
            } else {
                map[i][j] = false;
            }
        }
    }
    return s.slice(start, end + 1)
};

總結

通過這道題,鞏固了很多已經會的知識,手寫出三道算法,用時也不算短,很多時間花在了排查算法漏洞和修改漏洞上,漏洞這個東西,,還是練得少,熟能生巧以後應該會更早的發現漏洞吧。。
一題多解是一個很好的鍛鍊方式,可以通過一道題,鍛鍊自己各個方面的能力,發現自己的短板,同時也可以爲其他題提供新的思路。

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