leetcode-[自平衡二叉搜索树、桶思想] - 存在重复的元素III(220)

1、问题描述

给定一个整数数组nums,k,t,判断数组中是否存在两个不同的索引i,j,使得nums[i]和nums[j]之差的绝对值不超过t,i和j之差的绝对值不超过k。
例如:

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

2、解题思路

解决这道题需要找到一组满足以下两个条件的 i i i j j j

  1. ∣ i − j ∣ ≤ k |i - j| \le k ijk
  2. ∣ n u m s [ i ] − n u m s [ j ] ∣ ≤ t |nums[i] - nums[j]| \le t nums[i]nums[j]t

有很多的方法可以实现这一目标:
方法1:在滑动窗口内线性搜索
思想:每一个元素与它之前的 k k k个元素进行比较,查看它们的数值之差的绝对值是否在 t t t以内。具体实现时,维持一个大小为 k k k的滑动窗口,在这种情况下,第一个条件始终是满足的,只需要通过线性搜索检查第二个条件是否满足就可以了。

复杂度分析:
时间复杂度: O ( n × m i n ( k , n ) ) O(n\times min(k,n)) O(n×min(k,n)),其中每次搜索都需要花费 O ( m i n ( k , n ) ) O(min(k,n)) O(min(k,n))的时间,需要注意的是,每次搜索最多需要比较 n n n次,哪怕 k k k n n n大。
空间复杂度: O ( 1 ) O(1) O(1),额外开辟的空间为常数个。

方法2:将滑动窗口元素组织成自平衡的二叉搜索树
分析:方法1真正的瓶颈在于检查第二个条件是否满足时需要扫描滑动窗口内的所有元素,因此,我们需要考虑有没有比全扫描更好的方法;

  • 一种可能的想法是始终维持窗口内的元素有序,然后检查左右边界之差是否满足第二个条件,这种方法我们虽然可以在 l o g k logk logk的时间内找到元素插入位置, 但将该元素插入到正确位置时还是需要移动 O ( k ) O(k) O(k)个元素,所以时间复杂度还是 O ( k ) O(k) O(k)
  • 另一种想法是将滑动窗口内的元素组织成一个自平衡的二叉搜索树 s e t set set(大小为 k k k)。然后对于数组 n u m s nums nums中的每个元素 x x x,在 s e t set set中找到小于等于 x x x的最大数 g g g和大于等于 x x x的最小数 t t t,如果 x − g < = t x-g<=t xg<=t或者 s − x < = t s-x<=t sx<=t成立,则说明滑动窗口中含有满足第二个条件的两个元素,否则不含有。

思想:

初始化一棵空的自平衡的二叉搜索树set;
遍历nums中的每个元素x,并进行如下操作:
	在set上查找大于等于x的最小数s,如果s-x<=t , 则返回true;
	在set上查找小于等于x的最大数g,如果x-g<=t,  则返回true;
	在set中插入x;
	如果set的大小超过了k,则从set中去除最早加入树中的那个数;
	
返回false;

复杂度分析:
我们需要遍历这个长度为 n n n的数组,对于每次遍历,搜索、插入和删除操作都需要花费 O ( l o g k ) O(logk) O(logk)时间,故时间复杂度为 O ( n l o g k ) O(nlogk) O(nlogk)
空间复杂度:存储二叉搜索树所占的额外空间,为 O ( k ) O(k) O(k)

需要注意的是:

  • 当数组中的元素非常大的时候,进行数学运算可能造成溢出(比如2147483647 - (-1))。所以可以考虑使用支持大数的数据类型,例如 long。

  • C++ 中的 std::set,std::set::upper_bound 和 std::set::lower_bound 分别等价于 Java 中的 TreeSet,TreeSet::ceiling 和 TreeSet::floor。Python 标准库不提供自平衡 BST。

方法3:利用桶的思想在线性时间内解决问题
分析:受桶排序思想的启发,可以利用桶在线性时间内解决该问题。
桶排序的思想如下所示:
将元素划分到不同桶中,接着把每个桶再独立的使用不同的排序算法进行排序,最后按照桶的顺序收集所有元素便得到了一个有序数组。
比如对大小在1到100之间的8个数进行排序,首先,我们创建5个桶(区间),这5个桶分别包含的区间是 [ 1 , 20 ] 、 [ 21 , 40 ] 、 [ 41 , 60 ] 、 [ 61 , 80 ] 、 [ 81 , 100 ] [1,20]、[21,40]、[41,60]、[61,80]、[81,100] [1,20][21,40][41,60][61,80][81,100],这8个数中的任意一个都必然在一个桶中,对于值为 x x x的元素来说,其桶标签为 x / w x/w x/w,这里 w = 20 w=20 w=20。接着我们对每个桶内元素进行排序,最后按照桶的顺序收集所有元素。

接着我们通过一个与本问题及其相似的问题来引入算法的详细步骤:
我们把每个元素看成是一个人的生日,假设你的生日是3月份的某一天,现在需要知道班级中是否有人生日和你的生日间隔在 t = 30 t=30 t=30之内,在这里,我们假设每个月为30天,很明显,我们只需要检查所有生日在2月份、3月份、和4月份的同学就可以了。
之所以能这么做的原因在于,每个人的生日都属于一个桶,我们把这个桶称之为月份。每个桶包含的区间范围都是t,这能极大对简化我们的问题。很显然,任何不在同一个桶或相邻桶的两个元素之间的距离一定是大于t的。

我们把上述提到的桶的思想运用到这个问题中来,我们设计一些桶,让它们包含区间. . . , [ − 2 t − 2 , − t − 2 ] , [ − t − 1 , − 1 ] , [ 0 , t ] , [ t + 1 , 2 t + 1 ] , . . . ..,[-2t-2,-t-2],[-t-1,-1],[0,t],[t+1,2t+1],... ..,[2t2,t2],[t1,1],[0,t],[t+1,2t+1],...。我们把桶当作窗口,于是每次只需要检查 x x x所属的桶和相邻桶的元素,终于,我们可以在常量时间内解决在窗口内搜索的问题了。需要注意的是,本问题中每个桶都只包含一个元素,因为如果超过一个元素,则说明有两个元素的距离在t之内,可以直接得到问题的答案了。

思想:

初始化一个空桶map;
遍历nums的每个元素x,并进行如下操作:
	获取元素x所属的桶编号m=getid(x,w);
	如果桶m中已经含有一个元素,则直接返回true;
	如果桶m-1中含有一个元素y,并且y与x的距离小于t,则返回true;
	如果桶m+1中含有一个元素z,并且z与x的距离小于t,则返回true;
	把<m,x>插入map(在桶m为空,并且相邻桶没有重复元素的情况下);
	如果x的索引i大于k,从map中删除<getid(nums[i-k],w),nums[i-k]>;

复杂度分析:
时间复杂度:对于这n个元素中的任意一个来说,最多只需要做3次搜索、1次插入、1次删除,而这些操都是常量时间的,故时间复杂度为 O ( n ) O(n) O(n);
空间复杂度:需要开辟的额外空间主要是散列表,其大小和所包含的元素个数有关,故空间复杂度为 O ( k ) O(k) O(k).

3、代码实现

#include<map>
#include<cmath>
class Solution {
    
public:
    //获取x所在的桶的id,由于-3/5=0,而我们需要-3/5 = -1
    long getId(long x, long w){
            return x < 0 ? (x + 1)/w - 1 : x/w;
    }
    long Abs(long a , long b){
        return a < b ? b-a : a-b;
    }
    bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
        if(t < 0){
            return false;
        }
        map<long, long> bucket;
        long w = (long)t + 1;
        // map<long,long>::iterator iter;
        for(int i = 0; i < nums.size(); i++){

            // cout<<"第"<<i<<"步:"<<endl;
            // for(iter = bucket.begin(); iter != bucket.end(); iter++){
            //     cout<<"<"<<iter->first<<","<<iter->second<<">"<<endl;
            // }

            long id = getId(nums[i],w);
            if(bucket.find(id) != bucket.end()){
                return true;
            }
            if(bucket.find(id-1) != bucket.end() && Abs(bucket[id-1],nums[i]) <= t){
                return true;
            }
            if(bucket.find(id+1) != bucket.end() && Abs(bucket[id+1],nums[i]) <= t){
                return true;
            }
            bucket[id] = nums[i];
            if(i >= k){
                bucket.erase(bucket.find(getId(nums[i-k],w)));
            }
            
        }
        return false;
        
    }
   
};

【附加】leetcode同类型题目
217. 存在重复元素
219. 存在重复的元素 II

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Albert_YuHan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值