1. 程式人生 > >LeetCode #003# Longest Substring Without Repeating Characters(js描述)

LeetCode #003# Longest Substring Without Repeating Characters(js描述)

索引

問題描述:https://leetcode.com/problems/longest-substring-without-repeating-characters/

思路1:分治策略

感覺兩下半寫完了。。沒啥收穫,就別出心載先寫了個分治版本,結果很悲催。

按照演算法導論上的分治演算法框架,將原問題一分為二得到兩個形式完全相同的子問題,然後遞迴地解決子問題得到兩個子問題的最優解,最後合併兩個子問題的最優解得到原問題的最優解。“分解”步驟的時間複雜度是O(1),“解決”以及“合併”步驟的時間複雜度(似乎)是O(n^3),整體為O(n^3lgn),當然僅限我寫的版本。。。哈哈。。下面是用js寫的程式碼:

class CommonUtil {
    static lightCopy(obj) {
        let result = {}
        Object.assign(result, obj);
        
return result; } } // O(???), Runtime: 7792 ms, faster than 1.00% ... var lengthOfLongestSubstring = function(s) { if (s == "") return 0; let buildMaps = (symbol, p, q, r) => { let map = [], length = 0; let current = () => q - length; if (symbol == "right") { current
= () => q - 1 + length; } let init = () => { // 建立一個空物件,免去繁瑣的初始化過程 map[length++] = {}; }; let isSafe = () => { // 越界or字元重複返回false let out = symbol == "right" ? current() >= r : current() < p; let repeat = map[length - 1][s[current()]]; return !out && !repeat; }; let createMap = () => { // 複製map>>新增元素>>為下次呼叫做準備 map[length] = CommonUtil.lightCopy(map[length - 1]); map[length][s[current()]] = true; length++; }; init(); while (isSafe()) { createMap(); } return map; }; let combineMaps = (leftMaps, rightMaps, max) => { let compatible = (map1, map2) => { if (map1.length < map2.length) { let temp = map1; map1 = map2; map2 = temp; } let keys = Object.keys(map2); return !keys.some(x => map1[x]); }; let getPairs = (totalReduce) => { let reducePairs = []; for (let k = 0; k <= totalReduce; ++k) { reducePairs.push({ i: k, j: totalReduce - k, }); } return reducePairs; }; const leftLen = leftMaps.length - 1, rightLen = rightMaps.length - 1, baseLen = leftLen + rightLen; let sum = 0; let totalReduce = 0; while ((sum = baseLen - totalReduce) > max) { // 只檢測比子問題解的值更大的情形,由大往小找,一旦找到立刻停止搜尋 let reducePairs = getPairs(totalReduce); for (let k = 0; k != reducePairs.length; ++k) { let i = leftLen - reducePairs[k].i; let j = rightLen - reducePairs[k].j; if (i > 0 && j > 0 && compatible(leftMaps[i], rightMaps[j])) { return sum; } } totalReduce++; } return max; }; let findMaxCrossingSubstring = (p, q, r, subMaxLength) => { if (s[q - 1] == s[q]) { // 不可連線 return subMaxLength; } else { let leftMaps = buildMaps("left", p, q, r); let rightMaps = buildMaps("right", p, q, r); return combineMaps(leftMaps, rightMaps, subMaxLength); } }; let findMax = (start, end) => { if (start + 1 < end) { let mid = Math.floor((start + end) / 2); // Divide let maxLeft = findMax(start, mid); // Conquer let maxRight = findMax(mid, end); // Conquer const subMaxLength = Math.max(maxLeft, maxRight); // Combine return findMaxCrossingSubstring(start, mid, end, subMaxLength); // Combine } else { return 1; // basecase } }; return findMax(0, s.length); };

思路2:Brute Force - O(n^3)

js程式碼如下:

let satisfyCondition = (i, j, s) => {
    let map = {};
    for (let k = i; k <= j; ++k) {
        if (map[s[k]]) return false;
        map[s[k]] = true;
    }
    return true;
};

// O(n^3), Runtime: 880 ms, faster than 6.67% ...
var lengthOfLongestSubstring2 = function(s) {
    if (s == "") return 0;
    let max = 1,
        length, i, j;
    for (i = 0; i != s.length; ++i) {
        for (j = i + 1; j != s.length; ++j) {
            length = j - i + 1;
            if (length > max) {
                if (satisfyCondition(i, j, s)) {
                    max = length;
                } else {
                    break;
                }
            }
        }
    }
    return max;
};

思路3:動態規劃

因為最開始思路有點離譜,在這裡詳細記錄下寫了n個版本的心路歷程(從420ms~388ms~100ms)以待以後反思。。。。

O(n^2)版,錯解之一:420 ms

縮寫一個概念,便於之後描述↓

相容:當前字元與“由上一個字元開始的逆向最長子串”相容意味著上一個逆向最長子串裡不包含該字元,言下之意,當前字元可以直接插入上一個子串構成一個新解。

但凡最優化問題都很容易想到dp:問題顯然滿足最優子結構,規模為n的問題依賴於形式相同、但規模為n-1的子問題。遞迴地定義最優解的值:Mi = max(Mi-1, 新解new的值),然後考慮怎麼計算得到new的值,最簡單的想法是從當前字元倒回去算一遍,然而這樣太耗費時間,仔細想想,計算new的值其實和new-1是相關聯的,如果,當前字元與new-1相容,那麼直接在new-1的值的基礎上+1就好了,不相容才需要另行計算。

原始思路:完全依靠一個動態的hashMap的map[key]操作判斷當前字元與上一個最長子串的相容性。(每一個這樣的“上一個子串”都對應一個全新、特定的hashMap)

初代js程式碼如下:

var lengthOfLongestSubstring3 = function(s) {
    let m = [], // 儲存最優解的值,以長度作為下標
        b = [], // 儲存長度為i時,new的值
        p, r, map = {};

    let init = () => {
        b[0] = 0;
        m[0] = 0;
    };

    let work = () => {
        for (let len = 1; len <= s.length; ++len) {
            r = len - 1; // 引入r:將子問題規模len轉化為當前字元的下標
            if ((p = map[s[r]]) == undefined) { // 相容直接+1
                map[s[r]] = r;
                b[len] = b[len - 1] + 1;
            } else { // 不相容重新計算
                map = {};
                for (let i = p + 1; i <= r; ++i) {
                    map[s[i]] = i;
                }
                b[len] = r - p;
            }
            m[len] = Math.max(m[len - 1], b[len]);
        }
    };

    init();
    work();
    return m[s.length];
};

因為規模為n的問題實際僅依賴於規模為n-1的子問題,所以只需儲存上一輪迭代的相關值就行了。這是另外一個無差別版本(可讀性極差):

var lengthOfLongestSubstring4 = function(s) {
    let m = 0, b = 0;
    let p, r, map = {};
    for (let len = 1; len <= s.length; ++len) {
        r = len - 1;
        if ((p = map[s[r]]) == undefined) { // 可能為0
            map[s[r]] = r;
            b = b + 1;
        } else {
            map = {};
            for (let i = p + 1; i <= r; ++i) {
                map[s[i]] = i;
            }
            b = r - p;
        }
        m = Math.max(m, b);
    }
    return m;
};
View Code

O(n^2)版,錯解之二:388 ms

對建立新map做了點“然並卵”的優化(可讀性極差,其實這個時候我入了一個“縮短變數名可以提高几ms”的新坑,這是錯的!!!):

var lengthOfLongestSubstring5 = function(s) {
    let m = 0, b = 0, map = {}, p = 0, q, r;
    for (let len = 1; len <= s.length; ++len) {
        r = len - 1;
        if (s[r] in map) { 
            q = map[s[r]];
            if (q - p + 1 >= r - q - 1) {
                for (let i = p; i <= q; ++i) {
                    delete map[s[i]];
                }
            } else {
                map = {};
                for (let i = q + 1; i < r; ++i) {
                    map[s[i]] = i;
                }
            }
            map[s[r]] = r;
            b = r - q;
            p = q + 1;
        } else {
            map[s[r]] = r;
            b = b + 1;
        }
        m = Math.max(m, b);
    }
    return m;
};

O(n)版,思路轉變: 100 ms

before:

  1. 判定相容性:僅用map[key]判斷
  2. 維護hashMap:相容新增鍵值對,不相容重新建立hashMap並新增相應的鍵值對
  3. 在如何高效地動態修改hashMap上鑽牛角尖

after:

  1. 判定相容性:map[key]判斷+利用map[key]裡儲存的下標,同時維護“上一個最長子串的起始下標”構造一個簡單的判定條件
  2. 維護hashMap:相容新增鍵值對,不相容更新相應鍵的值就可以了(其實兩個都一樣)
  3. 通過新增條件而不是修改hashMap,簡單地篩掉hashMap的無效命中!

程式碼如下:

var lengthOfLongestSubstring6 = function(s) {
    let m = 0, lastIndex = {}, lastBegin = 0, currentIndex;
    for (let len = 1; len <= s.length; ++len) {
        currentIndex = len - 1;
        let ch = s[currentIndex];
        if (lastIndex[ch] !== undefined && lastIndex[ch] >= lastBegin) { // 不相容
            lastBegin = lastIndex[ch] + 1;
        }
        lastIndex[ch] = currentIndex;
        m = Math.max(m, currentIndex - lastBegin + 1);
    }
    return m;
};

現在看來這個思路很簡單、自然,為啥當時就想不到呢??歸根結底是經驗不足,但還是想總結下如何儘量避免“在錯誤的路上鑽牛角尖”以及“明明答案就在眼前,然而就是看不見”

  1. 在專注某個問題的時候,偶爾要跳出來(從整體上)整理一下思路
  2. 轉換角度
  3. 看別人的思路
  4. 多喝熱水。。

細節上的優化(Javascript限定)

借鑑了TOP1的神仙程式碼,終於拿到了“Runtime: 76 ms, faster than 99.37%”的成就:

var lengthOfLongestSubstring7 = function(s) {
    if (s.length < 2) return s.length;
    let max = 0, lastBegin = 0, code, characterIndex = new Array(255), fresh;
    for (let i = 0; i !== s.length; ++i) {
        code = s.charCodeAt(i);
        if (characterIndex[code] >= lastBegin) {
            lastBegin = characterIndex[code] + 1;
        }
        fresh = i - lastBegin + 1;
        if (fresh > max) {
            max = fresh;
        }
        characterIndex[code] = i;
    }
    return max;
};

js程式碼效能優化的小結:

  1. 歧途之一:縮短變數名絕對是得不償失的做法!大大降低可讀性的同時,對效能的提升幾乎為0(在網路上傳輸有專門的精簡程式碼工具,刷演算法題完全沒必要)
  2. 歧途之二:將函式手動內聯同樣不能提升效能(約等於0)
  3. 優化一:用if語句取代m = Math.max(m, xxxx);當m>xxxx成立時,採用if語句可以省去一次不必要的賦值。
  4. 優化二:當陣列長度能確定在某個範圍時用new Array(size)取代[]
  5. 優化三:當打算用HashMap的時候,以數字作為鍵的陣列>obj>new Map()

一些坑點&語法:

  1. 儘量不要用類似if(變數)作為條件判斷,因為有時0可能是變數的可行值之一,但是會被轉化成false
  2. 儘量用===取代==,避免轉型出錯,似乎對效能還有微乎其微的幫助。。。。
  3. 顯式地對null、undefined、NaN等值進行判斷,可以減少一些莫名其妙的bug
  4. undefined與任何數比較都返回false