[排序 Set] 217. 219. 220 存在重复元素 I II III(哈希表、Set模拟滑动窗口、TreeSet、桶排序)

217. 存在重复元素

题目链接:https://leetcode-cn.com/problems/contains-duplicate/(简单题)


分类:

  • 数组(排序 + 遍历)
  • 哈希表

在这里插入图片描述

题目分析

这题是本篇博客介绍的三个题目中最简单的一题,没有做任何限制,只需要寻找重复元素,所以方法有:

  • 思路1:对数组先排序,后遍历;
  • 思路2:使用哈希表存放已出现过的元素。

思路1:排序 + 遍历

先将数组排序,相同数字就会排在一起,遍历一次数组即可。

  • 注意:记得增加特殊用例 的判断
class Solution {
    public boolean containsDuplicate(int[] nums) {
    	//特殊用例的判断
        if(nums.length < 2) return false;
        Arrays.sort(nums);
        int pre = nums[0];
        for(int i = 1; i < nums.length; i++){
            if(pre == nums[i]) return true;
            else pre = nums[i];
        }
        return false;
    }
}
  • 时间复杂度:O(NlogN)
  • 空间复杂度:O(logN))

思路2:Set

使用一个哈希set保存每一个出现过的数字,如果遍历到一个set里存在的数字,说明遇到重复元素。

class Solution {
    public boolean containsDuplicate(int[] nums) {
        //特殊用例
        if(nums.length < 2) return false;
        Set<Integer> set = new HashSet<>();
        for(int i = 0; i < nums.length; i++){
            if(set.contains(nums[i])) return true;
            set.add(nums[i]);
        }
        return false;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

219. 存在重复元素 II

题目链接:https://leetcode-cn.com/problems/contains-duplicate-ii/


分类:

  • 哈希表
  • set模拟滑动窗口

在这里插入图片描述

题目分析

和217题相比,这题要查找的不仅是值相等的元素,还进一步要求这两个元素的下标差值的绝对值要 <= k,这相当于堵死了先排序后查找的思路,而哈希表的思路依然是可用的。

除此之外,思路2介绍了一种利用set来模拟滑动窗口的方法,更巧妙,还可以应用到220题上。

思路1:哈希表

使用一个哈希表,key=数值,value=下标,遍历数组时,未在map中出现的数字就加入map,并记录它的下标,在map中出现的数字就取出对应的value下标,计算两者的差值的绝对值:

  • 如果 <= k,则返回true;
  • 如果 > k,则把这个value更新为当前元素的下标,继续遍历数组,重复上面的工作。
class Solution {
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        if(nums.length < 2 || k <= 0) return false;
        Map<Integer, Integer> map = new HashMap<>();
        for(int i = 0; i < nums.length; i++){
            if(map.containsKey(nums[i])){
                int index = map.get(nums[i]);
                if(i - index <= k) return true;
            }
            map.put(nums[i], i);
        }
        return false;
    }
}

思路2:利用set构建滑动窗口(思路更巧妙)

开辟一个set,根据题目要求,让set存放的元素个数最多只能有k + 1个,如果set存放的元素超过k + 1个,就将当前set中最早加入的元素删除。

这相当于用set实现一个滑动窗口,窗口内存放的是数组中的k + 1个连续元素,这样如果两个值相等的元素还包含在同一个窗口内,就能保证两个元素的下标差值的绝对值一定是<=k。

算法流程为:当前待处理元素为nums[i],

  • 如果set存在当前值和nums[i]相等的元素,就认为找到一个下标差值 <= k的重复元素,返回true;
  • 如果set不存在该元素,就先判断set的元素个数,如果元素个数==k,就将最早加入的元素删除,然后该元素再加入,如果元素个数 < k,则直接加入即可。

如何找出当前set里最早加入的元素?

元素加入set的顺序就是按数组的排列顺序,设当前处理的元素是nums[i],则此时set滑动窗口中最早加入的元素就是nums[i - k - 1],所以可以指定删除set.remove(nums[i - k - 1])来删除最早加入的元素。

class Solution {
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        if(nums.length < 2 || k <= 0) return false;
        Set<Integer> set = new HashSet<>();
        for(int i = 0; i < nums.length; i++){
            if(set.contains(nums[i])) return true;
            if(set.size() == k) set.remove(nums[i - k]);
            set.add(nums[i]);
        }
        return false;
    }
}

220. 存在重复元素 III

题目链接:https://leetcode-cn.com/problems/contains-duplicate-iii/


分类:

  • 平衡二叉搜索树帮助查找(TreeSet)
  • 排序(HashMap实现桶排序)
  • 哈希表模拟滑动窗口

题目分析

本题在219 要求|i - j| <= k 的基础上,又增加了一个条件要求|nums[i] - nums[j]| <= t,其中第一个条件仍然可以用滑动窗口来解决,第二个条件的实现和优化是本题的考点。

思路1:暴力解

遍历数组,对于每个元素i,都枚举下标为[i+1 ~ i + k]中的每个元素,寻找是否存在与nums[i]差值 <= t的元素。

因为对元素 i 枚举的是 [i+1 ~ i + k] 的元素,所以一定满足条件1。

实现遇到的问题:字面量溢出问题

计算差值时可能出现字面量超过int范围,所以两个元素相减之前先分别转成long型:

long diff = Math.abs(((long)nums[i] - (long)nums[j]));

实现代码:

class Solution {
    public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        //特殊用例
        if(nums.length < 2 || k < 0 || t < 0) return false;
        for(int i = 0; i < nums.length; i++){
            for(int j = i + 1; j < nums.length && j <= i + k; j++){
                long diff = Math.abs(((long)nums[i] - (long)nums[j]));
                if(diff <= (long)t){
                    return true;
                }
            }
        }
        return false;
    }
}
  • 时间复杂度:O(N*K)

  • 存在的问题:用例超时。

思路2:TreeSet + 另一种角度寻找|差值|<=t的元素 (推荐)

思路1的性能消耗大部分在于 在窗口里寻找与nums[j]差值<=t的元素,思路1是暴力枚举,时间复杂度为O(K)。要比这个效率高,那只能是二分查找了。但二分查找需要建立的有序数组的基础上,所以问题转化为如何对窗口内的元素进行排序

  • 方法1(不可取):如果直接在数组上对窗口内的元素进行排序,窗口滑动时需要删除左边界元素,加入右边界元素,元素的加入有可能会导致整个数组的元素都需要移位,这样虽然查询时只需要O(logK),但元素的变动可能会带来O(K)的时间消耗,得不偿失。

  • 方法2:利用平衡二叉搜索树,把窗口内的元素拷贝存放在一棵二叉搜索树中,这样元素的插入和删除、查找都只需要O(logK)。注意:构造的二叉搜索树要平衡,否则性能就会下降为O(K)。

    其中,java提供了TreeSet,里面存放的数据是有序的,默认是自然顺序。

如何用set模拟滑动窗口的效果?

滑动窗口的大小为k+1,所以set存放的元素数量最多不能超过k+1,所以在新元素加入之前,先判断set存放的元素个数,如果set.size()>k,说明滑动窗口已满,需要先把最早加入的元素移除,设当前待处理元素为nums[i],则此时窗口内最早加入的元素是nums[i-k-1],需要将其移除:

set.remove(nums[i-k-1])。

如何使用TreeSet在滑动窗口中寻找差值的绝对值<=t的元素?( 另一种角度寻找|差值|<=t的元素)

这里使用了和思路1完全不一样的角度,设当前要处理的元素是x,TreeSet中存放了前k+1个元素。

TreeSet提供了ceiling函数,可以返回set中 >= 指定数值的最小元素,我们可以寻找set中大于等于 x - t 中最小元素,只要这个元素<=x+t,就说明找到一个元素满足:和x的差值的绝对值 <= t,返回true。
否则就将该元素加入set中。

实现遇到的问题:TreeSet存放的是包装类

TreeSet这类集合存放的元素都是对象,如果存放的是基本数据类型,也会自动装箱成对应的包装类,所以在调用ceiling寻找set中的目标元素时,虽然传入的参数是基本数据类型,但返回值要用对应的包装类来接收,否则会导致nullpointer错误,

  • ceiling如果找不到满足条件的元素就会返回null。

  • 包装类和基本数据类型比较时会自动拆箱。

class Solution {
    public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        //特殊用例
        if(nums.length < 2 || k < 0 || t < 0) return false;

        //使用TreeSet提供的平衡二叉搜索树结构
        TreeSet<Long> treeSet = new TreeSet<>();
        for(int i = 0; i < nums.length; i++){
            //如果当前set元素个数>k,就将最左端元素从set中移除
            if(i > k){
                treeSet.remove((long)nums[i - k - 1]);
            }
            Long minCeil = treeSet.ceiling((long)nums[i] - t);
            if(minCeil != null && minCeil <= (long)nums[i] + t) return true;
            treeSet.add((long)nums[i]);
        }
        return false;
    }
}
  • 时间复杂度:O(NlogK)
  • 空间复杂度:O(K)

思路3:桶排序(推荐)

桶排序的思想也可以用来在滑动窗口中寻找满足第二个条件的元素。题目要求对于元素nums[i],要在滑动窗口中找到|nums[i] - nums[j]| <= t,我们设置每个桶存放的t+1个元素,每个桶都有一个序号,例如:

桶0:[0,t],桶1:[t+1,2t+1],桶2:[2t+2,3t+2]

每个元素根据元素值大小进桶,这样做的好处是,桶内任意两个数之间的差一定是小于等于 t 的,所以一个元素nums[i]要进桶之前要先做判断:

  • 如果桶内已经有元素,说明找到一个满足条件的元素,返回true。

  • 如果桶内没有元素,则判断和这个桶相邻的左右两个桶内是否存在和nums[i]差值<=t的元素,如果有则返回true,如果都没有则将该元素加入对应的桶中。

    举个例子:

例如:k=3,t=2,有桶0:[0,2],桶1:[3,5],桶2:[6,8], nums=[1,7,5],
1,7分别加入桶0,桶2, 5要加入的是桶1,
但5加入之前,桶1内没有元素,所以判断相邻桶里是否存在差值<=t的元素,相邻的桶是桶0和桶2.
桶0里的|1-5|=4>t,桶2里的|7-5|=2<=t,所以5和7就是满足题目要求的两个元素,可以返回true.

用什么数据结构做桶?

由上面的算法设计可知,每个桶最多只需存放一个元素,且不需要预先把所有可能的桶都开辟出来,所以使用map做桶,key=桶序号,value存放元素。

如何根据元素值得到对应的桶序号?(难点)

例如:t=2,则有桶0:[0,2],桶1:[3,5],桶2:[6,8],可以发现桶序号是逢3进一,所以当元素值>=0时,桶序号=元素值/(t+1);

但是如果元素值是负数,则有所不同:(易忽略)

因为元素可能为负数,所以也完全可能存在桶序号为负的情况,例如:t=2,桶-1:[-3,-1],桶-2:[-6,-4],桶-3:[-9,-7],桶序号=(元素值+1)/(t+1) - 1, 元素值+1是为了把负元素值的范围设置成从0开始,外部再-1是因为桶序号0已经被占用了,所以存放负元素的桶序号从-1开始。

所以可以设置一个函数getId()用于根据元素值返回桶序号:

    //根据元素值获取桶序号
    public long getId(long value, long t){
        if(value >= 0) return value / (t + 1);
        else return (value + 1) / (t + 1) - 1; 
    }

如何用map模拟滑动窗口的效果?

前面的算法设计只满足题目要求的第二个条件,但还需要保证差值<=t的两个元素的下标差值<=k,可以设置一个滑动窗口,窗口内最多存放k+1个元素,当元素数量==k+1时,如果再有新的元素加入,就将当前窗口内最早加入的元素移除,这样就能保证窗口内元素下标和当前处理的元素下标差值始终<=k。

我们用map作为桶,map存放的元素个数 == 窗口存放的元素个数,所以当map.size() == k+1时,每新加入一个元素,就将最早加入的元素移除,设当前新加入的元素下标为i,则map内最早加入的元素下标就是i-k-1,将该元素移除。

  • 注意:map中要移除的不是nums[i-k-1],而是nums[i-k-1]对应的桶序号map.remove(getId(nums[i-k-1])。(易错点))
  • 可能存在int溢出问题,所以把map存放的元素类型设置为Long。

实现代码:

class Solution {
    public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        //特殊用例
        if(nums.length < 2 || k < 0 || t < 0) return false;

        //使用map作为桶
        Map<Long, Long> map = new HashMap<>();
        //枚举数组每个元素
        for(int i = 0; i < nums.length; i++){
            //如果map所模拟的滑动窗口大小==k+1,则先弹出最早加入到元素
            if(map.size() > k){
                map.remove(getId(nums[i - k - 1], t));
            }
            long idx = getId(nums[i], t);
            //如果对应桶里有元素存在,则说明找到满足条件的元素
            if(map.containsKey(idx)) return true;
            //如果相邻桶里有元素存在,且存在的元素和nums[i]的差值<=t,说明找到满足条件的元素
            if(map.containsKey(idx - 1) && Math.abs(map.get(idx - 1) - nums[i]) <= t) return true;
            if(map.containsKey(idx + 1) && Math.abs(map.get(idx + 1) - nums[i]) <= t) return true;
            //上述条件都不满足,则将当前元素加入对应桶中
            map.put(idx, (long)nums[i]);
        }
        return false;
    }
    //根据元素值获取桶序号
    public long getId(long value, long t){
        if(value >= 0) return value / (t + 1);
        else return (value + 1) / (t + 1) - 1; 
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值