存在重复元素 III

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

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

示例:

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

 

提示:

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

解题思路:

思路及算法

对于序列中每一个元素 xx 左侧的至多 kk 个元素,如果这 kk 个元素中存在一个元素落在区间 [x - t, x + t][x−t,x+t] 中,我们就找到了一对符合条件的元素。注意到对于两个相邻的元素,它们各自的左侧的 kk 个元素中有 k - 1k−1 个是重合的。于是我们可以使用滑动窗口的思路,维护一个大小为 kk 的滑动窗口,每次遍历到元素 xx 时,滑动窗口中包含元素 xx 前面的最多 kk 个元素,我们检查窗口中是否存在元素落在区间 [x - t, x + t][x−t,x+t] 中即可。

如果使用队列维护滑动窗口内的元素,由于元素是无序的,我们只能对于每个元素都遍历一次队列来检查是否有元素符合条件。如果数组的长度为 nn,则使用队列的时间复杂度为 O(nk)O(nk),会超出时间限制。

因此我们希望能够找到一个数据结构维护滑动窗口内的元素,该数据结构需要满足以下操作:

支持添加和删除指定元素的操作,否则我们无法维护滑动窗口;

内部元素有序,支持二分查找的操作,这样我们可以快速判断滑动窗口中是否存在元素满足条件,具体而言,对于元素 xx,当我们希望判断滑动窗口中是否存在某个数 yy 落在区间 [x - t, x + t][x−t,x+t] 中,只需要判断滑动窗口中所有大于等于 x - tx−t 的元素中的最小元素是否小于等于 x + tx+t 即可。

我们可以使用有序集合来支持这些操作。

实现方面,我们在有序集合中查找大于等于 x - tx−t 的最小的元素 yy,如果 yy 存在,且 y \leq x + ty≤x+t,我们就找到了一对符合条件的元素。完成检查后,我们将 xx 插入到有序集合中,如果有序集合中元素数量超过了 kk,我们将有序集合中最早被插入的元素删除即可。

注意

如果当前有序集合中存在相同元素,那么此时程序将直接返回 \texttt{true}true。因此本题中的有序集合无需处理相同元素的情况。

为防止整型 \texttt{int}int 溢出,我们既可以使用长整型 \texttt{long}long,也可以对查找区间 [x - t, x + t][x−t,x+t] 进行限制,使其落在 \texttt{int}int 范围内。

代码

C++JavaGolang

class Solution {
public:
    bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
        int n = nums.size();
        set<int> rec;
        for (int i = 0; i < n; i++) {
            auto iter = rec.lower_bound(max(nums[i], INT_MIN + t) - t);
            if (iter != rec.end() && *iter <= min(nums[i], INT_MAX - t) + t) {
                return true;
            }
            rec.insert(nums[i]);
            if (i >= k) {
                rec.erase(nums[i - k]);
            }
        }
        return false;
    }
};
复杂度分析

时间复杂度:O(n \log(\min(n, k)))O(nlog(min(n,k))),其中 nn 是给定数组的长度。每个元素至多被插入有序集合和从有序集合中删除一次,每次操作时间复杂度均为 O(\log(\min(n, k))O(log(min(n,k))。

空间复杂度:O(\min(n, k))O(min(n,k)),其中 nn 是给定数组的长度。有序集合中至多包含 \min(n, k + 1)min(n,k+1) 个元素。

方法二:桶
思路及算法

我们也可以使用利用桶排序的思想解决本题。我们按照元素的大小进行分桶,维护一个滑动窗口内的元素对应的元素。

对于元素 xx,其影响的区间为 [x - t, x + t][x−t,x+t]。于是我们可以设定桶的大小为 t + 1t+1。如果两个元素同属一个桶,那么这两个元素必然符合条件。如果两个元素属于相邻桶,那么我们需要校验这两个元素是否差值不超过 tt。如果两个元素既不属于同一个桶,也不属于相邻桶,那么这两个元素必然不符合条件。

具体地,我们遍历该序列,假设当前遍历到元素 xx,那么我们首先检查 xx 所属于的桶是否已经存在元素,如果存在,那么我们就找到了一对符合条件的元素,否则我们继续检查两个相邻的桶内是否存在符合条件的元素。

实现方面,我们将 \texttt{int}int 范围内的每一个整数 xx 表示为 x = (t + 1) \times a + b(0 \leq b \leq t)x=(t+1)×a+b(0≤b≤t) 的形式,这样 xx 即归属于编号为 aa 的桶。因为一个桶内至多只会有一个元素,所以我们使用哈希表实现即可。

struct HashTable {
    int key;
    int val;
    UT_hash_handle hh;
};

int getID(int x, long long w) {
    return x < 0 ? (x + 1ll) / w - 1 : x / w;
}

struct HashTable* query(struct HashTable* hashTable, int x) {
    struct HashTable* tmp;
    HASH_FIND_INT(hashTable, &x, tmp);
    return tmp;
}

bool containsNearbyAlmostDuplicate(int* nums, int numsSize, int k, int t) {
    struct HashTable* hashTable = NULL;
    for (int i = 0; i < numsSize; i++) {
        long long x = nums[i];
        int id = getID(x, t + 1ll);
        struct HashTable* tmp;
        tmp = query(hashTable, id - 1);
        if (tmp != NULL && fabs(x - tmp->val) <= t) {
            return true;
        }
        tmp = query(hashTable, id + 1);
        if (tmp != NULL && fabs(x - tmp->val) <= t) {
            return true;
        }
        tmp = query(hashTable, id);
        if (tmp != NULL) {
            return true;
        } else {
            tmp = malloc(sizeof(struct HashTable));
            tmp->key = id;
            tmp->val = x;
            HASH_ADD_INT(hashTable, key, tmp);
        }
        if (i >= k) {
            tmp = query(hashTable, getID(nums[i - k], t + 1ll));
            HASH_DEL(hashTable, tmp);
        }
    }
    return false;
}

 

 

 

上一篇:VScode反向搜索突然失效解决方案


下一篇:中国视频云千亿市场,阿里云四年稳居“第一”