壹 ❀ 引
今天的题目来自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=3
,t=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=3
,t=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个小时,确实很绕...如果有幸看到这篇题解,还是静下心来理一理,那么本文结束。