【C/C++】220. 存在重复元素 III

142 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第25天,点击查看活动详情


题目描述

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

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

提示:

  • 0nums.length21040 \leqslant nums.length \leqslant 2 * 10^4
  • 231nums[i]2311-2^{31} \leqslant nums[i] \leqslant 2^{31} - 1
  • 0k1040 \leqslant k \leqslant 10^4
  • 0t23110 \leqslant t \leqslant 2^{31} - 1

示例 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

题意整理

题目给定一个整数数组 nums,要求从数组中找到两个下标不同的整数 nums[i]nums[j]i ≠ j),满足值和下标之差都在给定的范围内( 满足 abs(nums[i] - nums[j]) <= t 同时又满足 abs(i - j) <= k )。

解题思路分析

习惯性动作,做题之前我们都要先看题目给我们数据范围:

  • 数组长度为 2e4O(n2)O(n^2) 暴力会 TLE 超时;
  • 数据范围在 INT 范围内,其中包含 INT 最大值和最小值边界,这时候需要注意数据 溢出问题

回到题目,对于满足给定 范围 的题目,我们通常会想到使用 滑动窗口 来实现,但是现在给定的范围有两个,而滑动窗口只能保证满足其中一个范围,那我们考虑如何在满足其中一个范围区间内寻找满足另一个范围区间。

由于题目给定的是无序数组,所以我们考虑首先满足 下标 之差在给定范围:

重复元素 (1).jpg

我们固定这样一个长度为 k 的滑动窗口,在区间里找到差值也在给定范围内的一对数。如果每次都 O(k)O(k) 遍历寻找,那么时间复杂度为 O(kn)O(k*n) ,也会 TLE 超时。因此我们希望能够找到一个数据结构维护滑动窗口内的元素。

这里我们可以使用 set 集合容器来维护,set 集合容器支持添加删除操作,因为内部有序的特点,所以同时支持二分查找,这样我们可以以 O(log2k)O(\log_2 k) 的时间判断滑动窗口中是否存在满足条件的元素。

具体实现(滑动窗口 + 有序集合 set

  1. O(n)O(n) 遍历数组,当遍历到 nums[i] 时,判断集合中是否存在同时满足值和下标之差都在给定的范围内的 nums[j]
    • 因为在集合中的元素都是满足下标之差在给定的范围内的,所以只用判断能否找到值之差在给定范围内的 nums[j] 即可。
    • 对于 nums[i] 来说,落在区间 [nums[i] - t, nums[i] + t]nums[j] 都是满足条件的,在集合中我们使用 set 集合容器内置函数 lower_bound(nums[i] - t) 函数二分查找第一个大于等于 nums[i] - t 的元素,如果能够找到这样的元素,同时还需判断是否小于等于 nums[i] + t,如果同时满足就返回 true
  2. 否则将 nums[i] 放入集合中,同时需要维护集合中元素个数保持在小于等于 k - 1 个,这样才能保证下一个 nums[i] 和集合中的元素下标之差小于等于 k

需要注意的是题目数据涉及到 INT 数据范围边界,为防止溢出,我们可以使用更大的数据类型进行存储,也可以对数据进行限制,使其落在 INT 范围内。

因为 set 集合自动去重,我们需要考虑 nums[i] 与集合中元素相同的情况,我们可以发现这种情况是返回 true 的,所以无需处理相同元素的情况。

复杂度分析

  • 时间复杂度:O(nlog2(min(n,k)))O(n \log_2(\min(n, k))),其中 n 是给定数组的长度。每个元素至多被插入有序集合和从有序集合中删除一次,每次操作时间复杂度均为 O(log2(min(n,k))O(\log_2(\min(n, k))
  • 空间复杂度:O(min(n,k))O(\min(n, k)),其中 n 是给定数组的长度。有序集合中至多包含 min(n,k+1)\min(n, k + 1) 个元素。

代码实现

class Solution {
public:
    bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
        //s集合用于存放 下标差值在 k 以内的数
        set<int> s;
        s.clear();
        int n = nums.size();
        for(int i = 0; i < n; i++){
            //对于当前nums[i],寻找集合中在 [nums[i] - t, nums[i] + t] 区间上的值
            //设置下限 nums[i] - t,为了防止溢出题目数据,因为题目数据是在INT范围内的,所以可以特殊处理一下
            int lowerLimit = max(nums[i], INT_MIN + t) - t;
            //同理设置上限
            int upperLimit = min(nums[i], INT_MAX - t) + t;
            //在集合中查找第一个大于等于lowerLimit的迭代器
            auto iter = s.lower_bound(lowerLimit);
            //迭代器不等于s.end()表示找到大于等于lowerLimit的值了,同时判断是否小于等于upperLimit
            if(iter != s.end() && *iter <= upperLimit) return true;
            //将值压入集合,不会压入相同元素,因为如果在集合中找到相同元素会直接返回true
            s.insert(nums[i]);
            //如果下标差值大于k时,每次需要将下标为i - k的值删除
            if(i >= k) s.erase(nums[i - k]);
        }
        return false;
    }
};

总结

  • 该题利用滑动窗口的思想,用有序的 set 集合容器来维护完成滑动窗口的操作,使用二分查找来查找满足条件的值,这样就可以同时满足值和下标之差都在给定的范围内。同时需要注意数据溢出问题。
  • 该题还可以利用桶排序的思想解决(更优),按照元素的大小进行分桶,维护一个滑动窗口内元素所对应的元素。时间复杂度:O(n)O(n),其中 n 是给定数组的长度。每个元素至多被插入哈希表和从哈希表中删除一次,每次操作的时间复杂度均为 O(1)O(1)

结束语

生活是美好的。只有心中有阳光,每一天都充满生机;只要心中有花香,每一个平淡的日子都有诗意。去拥抱世界吧,让今天过得充实,让明天充满希望。