zoukankan      html  css  js  c++  java
  • JS Leetcode 220. 存在重复元素 III 题解分析,暴力解法与桶排序

    壹 ❀ 引

    今天的题目来自LeetCode 220. 存在重复元素 III,难度中等,题目描述如下:

    给你一个整数数组 nums 和两个整数 k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k 。

    如果存在则返回 true,不存在返回 false。

    示例 1:

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

    示例 2:

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

    示例 3:

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

    提示:

    • 0 <= nums.length <= 2 * 104
    • -231 <= nums[i] <= 231 - 1
    • 0 <= k <= 104
    • 0 <= t <= 231 - 1

    贰 ❀ 暴力解法

    我们简单分析题意,给定一个数组以及两个整数k与t,判断是否存在两个不同的下标i,j,满足abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k,存在返回true,反之返回false。

    那么根据提议,我们完全可以根据题目要求的条件直接暴力解决:

    /**
     * @param {number[]} nums
     * @param {number} k
     * @param {number} t
     * @return {boolean}
     */
    var containsNearbyAlmostDuplicate = function (nums, k, t) {
        // i从0开始
        for (let i = 0; i < nums.length; i++) {
            // j永远从i后一位开始
            for (let j = i + 1; j < nums.length; j++) {
                // 直接抽取题目要求的条件
                if (Math.abs(nums[i] - nums[j]) <= t && Math.abs(i - j) <= k) {
                    return true;
                };
            };
        };
        return false;
    };
    

    代码就不解释了,暴力解法非常简单。

    贰 ❀ 桶排序

    老实说,桶排序的做法比较难理解(当然也许是因为我太菜了),我在题解区逛了一圈,发现大部分题解说的基本都半知半解,看的我是非常难受和....也不知道是不是语言差异,部分思路在JS中也行不通,这里我也是读了好几篇题解,综合了一下他们的思想,整理下我最终的理解,其中思路来源主要来自C++ 利用桶分组, 详细解释

    在我贴的参考思路题解中,举了一个我觉得十分恰当的例子,这里我简单重复下,假设有一批学生都是同一年不同月出生,老师要找出出生期相差在30天以内的学生,这里的30天可以理解为题目中的参数t,即两个学生出生日期的差小于等于30。那么我们大致能掌握这样一个规律:

    • 同一个月出生的学生一定满足条件,比如3月5号,3月15,都在一个月内总不能超出一个月的时间差吧。
    • 某个月相邻的前后两个月的学生可能满足条件,比如3月5号,2月15号就满足一个月内,但2月1号就不行了,超出了一个月,所以是可能满足。
    • 间隔超过2个月的学生绝对不可能满足条件,比如3月出生的学生和1月或者5月的学生都不可能满足条件。

    那么知道了这个规则,我们要做的就是将这些学生按月份进行分配,3月出生的学生都在一起,4月的也都在一起,这一个个月份就像一个桶,我们将学生装进了桶里。相同桶里学生的日期差一定小于30(t),我们可以总是至多维护3个桶,这个3可以理解为题意中的参数k,因为规则2也说了,维护四个桶没意义,要找有没有符合条件的还是得从自己,活着相邻的桶里去看有没有符合规则的。

    OK,上面的例子整体会比较抽象,但大致阐述了这么一个想法,你能大概明白是什么意思就足够了,那么我们现在要思考一个问题,我们怎么知道哪些学生应该放在某个桶呢?

    这里我先给出一个公式,再论证它:

    // x可以理解成学生的出生日期,t可以理解成我们定下的规则,也就是小于等于30天
    let bucketNum = Math.floor(x / (t + 1))
    

    现在开始论证,我们假设学生的年龄为[0,1,2,3,4,5,6],t是3,即数字之间相差小于等于3的应该放在一起。

    Math.floor(0 / (3 + 1))//0
    Math.floor(1 / (3 + 1))//0
    Math.floor(2 / (3 + 1))//0
    Math.floor(3 / (3 + 1))//0
    
    Math.floor(4 / (3 + 1))//1
    Math.floor(5 / (3 + 1))//1
    Math.floor(6 / (3 + 1))//1
    

    你会发现[0,1,2,3]四个数在这个公式中都等于0,说明它们四个应该放在一起,而且仔细推敲,这四个数的差的绝对值还真是小于等于3。但是4就不能加进去了,因为4-0>3[4,5,6]同理又被分配在了一个桶里。

    我看一些题解给的公式是x / t然后又说要加1,但没具体说为什么要加个1,其实根据题意中0 <= t <= 231 - 1,假设t=0,万物除以0都是无限大,就无法区分了,而且站在JS的角度,如果我们不加1,你会发现[0,1,2]会被丢在一个桶,因为:

    Math.floor(0 / 3)//0
    Math.floor(1 / 3)//0
    Math.floor(2 / 3)//0
    Math.floor(3 / 3)//1
    

    3根本就不会放进0号桶,这根本就满足不了条件了,所以公式是一定得加个1(这也是为什么我看一些人题解看的特别恼火的原因,写的不明不白,看的很无语)。

    那么我们假设有数组[1,2,3,1]k=3t=0,即在这个数组中,是否存在两个数的下标差绝对值小于等于3,且两个数的差的绝对值小于等于0。我们尝试推导这个过程:

    i=0时,取到数字1,由于Math.floor(1 / (0 + 1))为1,我们需要创建1号桶,并把0这个元素作为value存起来。

    i=1时,取到数字2,由于Math.floor(2 / (0 + 1))为2,桶不同说明数字相差绝对超过2了,如果满足0,绝对在同一个桶,一样创建2号桶,把2存起来

    继续,当i=2时,取到了数字3,由于Math.floor(3 / (0 + 1))为3,说明相差又超过0了,继续上面的操作。

    i=3时,取到了数字1,由于Math.floor(3 / (0 + 1))为0,我们发现0号桶已经存在了,说明0号桶和当前的数字相差一定小于等于0,而且i此时为3,3并不比k大,说明下标没超过条件,满足最终返回true。

    等等,这个例子好像过于理想,假设我们再多一点呢?比如[1,2,3,4,1],k和t不变:

    前三步还是一样,创建了1,2,3,一共三个桶。

    i=3时,由于Math.floor(4 / (0 + 1))为4,所以创建了4号桶,我们发现此时仍然没找到符合条件的数字,注意由于此时i>=k已经是下标差的最大极限了,在最大极限情况下还没找到满足条件,我们得删除掉一个桶。为什么?你想想,假设我们不删除,走到i=4时,由于Math.floor(1 / (0 + 1))为1,我们发现它和1=0时得到的是相同的桶,那么说明两个元素差的绝对值一定<=t,但是很遗憾i>k了,此时的i是4,所以就算桶相同,你的下标已经不符合了啊,那这个桶的比较又有什么意义呢?所以在i>=k的时候,此时你的桶还有参与比较的资格,但如果还不满足,那就得删除掉最早创建的桶,为下次比较做准备。

    有同学就有疑问了,删掉了不会对后续的比较产生影响吗?我们假设[1,2,3,4,1,1],kt不变,前面几步还是相同,当i=3时不满足,比较完成我们把i=0创建的1号桶给删除了。当i=4其实又创建了1号桶,而当i=5我们还是找到了符合规则的数字,最终返回了true。

    以上长篇大论,其实我们还只是推导了学生出生日期的规则1,即如果一个桶被重复创建两次(两个数字在一个桶和一个桶被创建两次是同一个意思),然后下标差还小于等于k,那么这两个数一定符合条件。

    别忘了,我们还有规则2,也就是去相邻的桶里找。比如[3][4,5,6]k=3t=1,3和4虽然在不同的桶,但是它们缺满足下标差以及数字差的条件限制。

    总结一下:

    当创建一个桶,如果桶已存在(说明数字差小于等于t),且下标没超出k,那就返回true。

    如果创建一个桶,桶不存在,那就创建好这个桶,记录好数字,同时看看左右相邻的桶里的数求差,看看能不能满足条件。注意,左右相邻的桶一定也只会有一个数字,为啥呢?因为如果左右相邻的桶存在2个数字,那就满足了上一条规则,已经返回true了...

    如果以上都比较完了,这时候看看i跟k的关系,如果满足了i>=k,那你得删除最早创建的桶了。

    那么,贴上代码:

    /**
     * @param {number[]} nums
     * @param {number} k
     * @param {number} t
     * @return {boolean}
     */
    var containsNearbyAlmostDuplicate = function (nums, k, t) {
        // 计算桶编号
        function getBucketNum(x) {
            return Math.floor(x / (t + 1));
        };
        // 创建一个大桶,里面用于存放一些小桶
        let buckets = new Map();
        for (let i = 0; i < nums.length; i++) {
            // m是当前遍历元素将要在的桶
            const bucket = getBucketNum(nums[i]);
            // 此时桶的数量一定是被维护好的,如果一个桶已经存在,说明一定满足条件。
            if (buckets.has(bucket)) {
                return true;
                // 比较右边的桶,看看差是否满足条件
            } else if (buckets.has(bucket + 1) && Math.abs(buckets.get(bucket + 1) - nums[i]) <= t) {
                return true;
                // 同理比较左边
            } else if (buckets.has(bucket - 1) && Math.abs(buckets.get(bucket - 1) - nums[i]) <= t) {
                return true;
            }
            // 保存这个桶,以及桶的数字
            buckets.set(bucket, nums[i]);
            // 如果i>=k,能走到这一步,满足条件的最后一个桶都比较完了,还不符合,那就得删除最早的桶,为下次比较做准备
            if (i >= k) {
                buckets.delete(getBucketNum(nums[i - k]));
            }
        }
        return false;
    };
    

    这道题桶排序的题解,我想想,前前后后大概整理加思考了差不多花了4个小时,确实很绕...如果有幸看到这篇题解,还是静下心来理一理,那么本文结束。

  • 相关阅读:
    some tips
    ORA00847: MEMORY_TARGET/MEMORY_MAX_TARGET and LOCK_SGA cannot be set together
    Chapter 01Overview of Oracle 9i Database Perfomrmance Tuning
    Chapter 02Diagnostic and Tuning Tools
    变量与常用符号
    Chapter 18Tuning the Operating System
    标准输入输出
    Trace files
    DBADeveloped Tools
    Chapter 03Database Configuration and IO Issues
  • 原文地址:https://www.cnblogs.com/echolun/p/14683541.html
Copyright © 2011-2022 走看看