给你一个整数数组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,5,9,1,5,9], k = 2, t = 3
输出:false
思路:滑动窗口 + 有序集合
对于序列中每一个元素x左侧的至多k个元素,如果这k个元素中存在一个区间[x - t, x + t]中,我们就找到了一对符合条件的元素。注意到对于两个相邻的元素,它们各自的左侧的k个元素中有k - 1个是重合的。于是我们可以使用滑动窗口的思路,维护一个大小为k的滑动窗口,每次遍历到元素x时,滑动窗口中包含元素x前面的最多k个元素,检查窗口中是否存在元素落在区间[x - t, x+ t]。
如果使用队列维护滑动窗口内的元素,由于元素是无序的,我们只能对于每个元素都遍历一次队列来检查是否有元素符合条件。如果数组的长度为n,则使用队列的时间复杂度为O(nk),会超出时间限制。
- 支持添加和删除指定的操作,如insert和erase操作,否则我们无法维护滑动窗口
- 内部元素有序,支持二分查找的操作,这样我们可以快速判断滑动窗口中是否存在元素满足条件,具体而言,对于元素x,当我们希望判断滑动窗口中是否存在某个数y落在区间[x - t, x + t]中,只需要判断滑动窗口中所有大于等于x - t的元素中的最小元素是否小于等于x + t即可。
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++)
{
// lower_bound在set中用法:
// 二分查找一个有序数列,返回第一个大于等于x的数,如果没找到,返回末尾的迭代器位置
// 比如刚开始rec.lower_bound(nums[i] - t)就是在找滑动窗口rec当中,大于等于nums[i] - t的第一个数,也就是左端点。很明显,一开始rec为空,取lower_bound为0,i = 1,送入一个1之后,rec.lower_bound(2)不存在,返回最后一个数字1,继续插入。由于我们的t=0,每次都是查询跟当前元素相等的元素,作为lower_bound,所以一直都无法得到,直到最后一个数字,1跟最一开始的1,可以查询到,而且还不是最后一个数字。然后进行下一步的判断,1,还小于等于min(1)+0。
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(nlog(min(n, k))),其中
- 空间复杂度:O(min(n, k)),其中n是给定数组的长度。有序集合中至多包含min(n, k + 1)个元素。