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

猜你喜欢

转载自www.cnblogs.com/xkxf/p/10230703.html