• JS leetcode 存在重复元素 II 题解分析,记一次震惊的负向优化


    壹 ❀ 引

    整理下今天做的算法题,题目难度不高,但在优化角度也是费了一些功夫。题目来自219. 存在重复元素 II,问题描述如下:

    给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。

    示例 1:

    输入: nums = [1,2,3,1], k = 3
    输出: true
    

    示例 2:

    输入: nums = [1,0,1,1], k = 1
    输出: true
    

    示例 3:

    输入: nums = [1,2,3,1,2,3], k = 2
    输出: false
    

    题目意思其实很简单,看一个数组中是否有两个元素相等,且后者索引减去前者的差集,要小于等于数值k。注意不是等于k,我第一次提交就是看错了直接给挂了,接下来我们来说说怎么做。

    贰 ❀ 暴力解法

    我首先想到的自然是for循环遍历嵌套,用两个索引分别表示数组中一前一后的元素,如果两个元素相等,且后者索引减去前者索引的值<=k,则返回true即可:

    /**
     * @param {number[]} nums
     * @param {number} k
     * @return {boolean}
     */
    var containsNearbyDuplicate = function (nums, k) {
        let ans = false
        for (let i = 0; i < nums.length; i++) {
            // 注意这里j从i+1开始
            for (let j = i + 1; j < nums.length; j++) {
                // 满足两数相等,且索引差不大于k即可
                if (nums[i] === nums[j] && j - i <= k) {
                    return true;
                };
            };
        };
        return ans;
    };
    

    叁 ❀ 震惊的负向优化

    我在上篇文章中提到,如果能将时间复杂度从O(n²)降到O(n),那将是大大的优化,尽管上述代码遍历也并未达到n²,但我们还是可以试试。

    在阅读了官方推荐的哈希表做法后,我实现了如下代码:

    /**
     * @param {number[]} nums
     * @param {number} k
     * @return {boolean}
     */
    var containsNearbyDuplicate = function (nums, k) {
        // 我们始终维护一个大小为k的哈希表
        let hash = [];
        for (let i = 0; i < nums.length; i++) {
            // 判断有没有当前元素,开始为空肯定没有
            if (hash.includes(nums[i])) {
                return true;
            };
            // 将当前元素加入哈希表中
            hash.push(nums[i]);
            // 前面说了,哈希表大小始终为k,超过了,我们就删除最旧的数据
            if (hash.length > k) {
                hash.shift();
            };
        };
        return false;
    };
    

    实现不难理解,我们始终维护一个大小为k的哈希表,并依次将元素加入表内,由于一开始为空,自然是加入第一个。从第二个开始判断有没有,如果有自然返回true。反之没有继续加入表内。但需要注意的是,我们的表的大小是k,一旦超过我们就得删除掉最旧的数据。

    思路清晰,然后我提交了代码,一看执行时间,我人都傻了,出于怀疑我又点了一次提交(2000ms与1780ms)。

    从表面上看,暴力解法用了两次循环嵌套,而哈希表做法只用了一次。其实从内部实现来看,循环嵌套做的事情要简单的多。

    我们知道数组是呈线性排列的一种数据结构,当我们通过索引直接访问某条数据时,它的时间复杂度为O(1),而做查找操作就不同了,由于没有提供标识,我们只能通过线性查找一个接一个进行对比,看看当前是不是我们想要的。看看hash.includes(nums[i])这行代码,你是否意识到了什么呢?

    除此之外,我们在下方代码还做了shift操作,也就是数组删除,由于数组是连续的,出现空缺就得填补,像这样:

    所以整体上来看,双循环嵌套执行次数看着虽然多,但站在时间复杂度角度来说,每一次操作单元耗时微乎其微,都是根据索引直接找到对应元素对比。而后一种实现每次遍历做的事情就非常耗时了。

    肆 ❀ 使用ES6 set结构

    奇妙的是,同样的思路,我们将数组换成set结构,速度就快了很多,只用了88ms,这里引用灵魂画手的实现:

    /**
     * @param {number[]} nums
     * @param {number} k
     * @return {boolean}
     */
    var containsNearbyDuplicate = function(nums, k) {
        // 创建哈希表
        const set = new Set();
        for(let i = 0; i < nums.length; i++) {
            // 判断有没有
            if(set.has(nums[i])) {
                return true;
            };
            set.add(nums[i]);
            if(set.size > k) {
                // 删除最旧数据
                set.delete(nums[i - k]);
            };
        };
        return false;
    };
    

    可以看到只是单纯换了数据结构,执行用时质的提升,这里我不禁对于数据结构差异产生了兴趣,在知乎javascript 里的Set.has和Array.includes谁的效率更高?提问中,有用户做过测试,在大量数据下,set要远高于数组。但本质原因我一时无法考证了,只能在心里埋下一枚问题种子,待日后算法与数据结构的不算学习,希望能给自己一个答案。

    那么到这里,本文正式结束。

  • 相关阅读:
    16、cgminer学习之:popen函数和system函数详解(执行系统命令)
    16、cgminer学习之:pthread_mutex_init和pthread_cond_init
    15、python学习手册之:元组、文件及其他
    15、python学习手册之:列表和字典
    Chorme浏览器中使用flash debug版本
    Best Practices
    Titled-Contributing to Tiled
    Titled
    VS2010配合SVN搭建使用
    Google code问题记录
  • 原文地址:https://www.cnblogs.com/echolun/p/13280245.html
Copyright © 2020-2023  润新知