220. 存在重复元素 III
给你一个整数数组 nums
和两个整数 k
和 t
。请你判断是否存在 两个不同下标 i
和 j
,使得 abs(nums[i] - nums[j]) <= t
,同时又满足 abs(i - j) <= k
。
如果存在则返回 true
,不存在返回 false
。
示例 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
提示:
0 <= nums.length <= 2 * 10^4
-2^31 <= nums[i] <= 2^31 - 1
0 <= k <= 10^4
0 <= t <= 2^31 - 1
方法一:TreeSet
本题的暴力法不难想到,对于每一个数字,都去遍历其前面的 k
个数,寻找相减的绝对值小于等于 t
的数。这样的时间复杂度是 O(nk)
,一共 n
个数字,每个数字要匹配 k
次,最后会超时。
每个数字至少要遍历一次,所以 n
无法减小,只有减小 k
。在前 k 个数字中,我们只关心与当前数字最接近的数。平衡二叉树能够以 O(logk)
的时间复杂度提供 删除元素/查找最接近元素 的功能,这里直接使用 TreeSet。
参考代码
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
int n = nums.length;
TreeSet<Long> treeSet = new TreeSet<>();
for (int i = 0; i < n; i++) {
long val = nums[i];
// 寻找大于等于 val 的
Long up = treeSet.ceiling(val);
if (up != null && up - val <= t) {
return true;
}
// 寻找小于 val 的
Long down = treeSet.floor(val);
if (down != null && val - down <= t) {
return true;
}
treeSet.add(val);
// 剔除过期元素
if (i >= k) {
treeSet.remove((long) nums[i - k]);
}
}
return false;
}
执行结果
方法二:桶排序
方法一的执行结果感人,因为处理每个元素依然需要 n(logk)
的时间复杂度。我们可以利用「桶排序」把处理每个元素的时间复杂度降为 O(1)
。
差值是 t
,每个桶的大小即为 t + 1
。在窗口 k
内,有以下两种情况可以返回 true
。
- 当元素入桶时,发现桶里已经有元素了。因为桶内的最大差值为 t,所以满足条件。
- 左边或右边的桶内有元素且和当前元素绝对值小于等于 t 。
参考代码
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
int n = nums.length;
long size = t + 1L;
// 负数不好计算 桶id,先把数字全部转成正数。
long[] longs = new long[n];
for (int i = 0; i < n; i++) {
longs[i] = 0x80000000L + nums[i];
}
Map<Long, Long> map = new HashMap<>();
for (int i = 0; i < n; i++) {
long idx = longs[i] / size;
// 当前桶有元素,直接返回 true
if (map.containsKey(idx)) {
return true;
}
// 左右的桶有没有满足条件的元素
if ((map.containsKey(idx - 1) && Math.abs(map.get(idx - 1) - longs[i]) <= t) ||
(map.containsKey(idx + 1) && Math.abs(map.get(idx + 1) - longs[i]) <= t)) {
return true;
}
// 把元素放入桶 并 剔除过期元素
map.put(idx, longs[i]);
if (i >= k) {
map.remove(longs[i - k] / size);
}
}
return false;
}
执行结果