赞
踩
滑动窗口是算法中一种重要的解题思想,通过分析力扣中的几道题目来学习滑动窗口的思想。
题目链接:220. 存在重复元素 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,5,9,1,5,9], k = 2, t = 3
输出:false
题目分析:
根据题意,对于任意一个位置 i
(假设其值为 u
),我们其实是希望在下标范围为 [max(0, i-k), i]
内找到值范围在 [u-t, u+t]
的数。
一个朴素的想法是每次遍历到任意位置 i
的时候,往后检查 k
个元素,但这样做的复杂度是O(nk)
的,会超时。
显然我们需要优化「检查后面 k 个元素
」这一过程。
我们希望使用一个「有序集合」去维护长度为 k 的滑动窗口内的数,该数据结构最好支持高效「查询」与「插入/删除」操作:
或许你会想到近似 操作的 HashMap
,但注意这里我们需要找的是符合abs(nums[i] - nums[j]) <= t
的两个值,nums[i] 与 nums[j]
并不一定相等,而 HashMap 无法很好的支持「范围查询」操作。
我们还会想到「树
」结构。
例如 AVL,能够让我们在最坏为 O(logk)的复杂度内取得到最接近 u 的值是多少,但本题除了「查询」以外,还涉及频繁的「插入/删除」操作(随着我们遍历 nums 的元素,滑动窗口不断右移,我们需要不断的往「有序集合」中删除和添加元素)。
简单采用 AVL 树,会导致每次的插入删除操作都触发 AVL 的平衡调整,一次平衡调整会伴随着若干次的旋转。
而红黑树则很好解决了上述问题:将平衡调整引发的旋转的次数从「若干次」限制到「最多三次」
。
因此,当「查询」动作和「插入/删除」动作频率相当时,更好的选择是使用「红黑树」。
也就是对应到 Java 中的 TreeSet 数据结构
(基于红黑树,查找和插入都具有折半的效率)。
实现方面,我们在有序集合 TreeSet
中查找大于等于 x−t
的最小的元素 y
,如果 y
存在,且 y ≤ x+t
,我们就找到了一对符合条件的元素。完成检查后,我们将 x
插入到有序集合中,如果有序集合中元素数量超过了 k
,我们将有序集合中最早被插入的元素删除即可。
其他细节:由于 nums 中的数较大,会存在 int 溢出问题
,我们需要使用 long
来存储。
class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
// TreeSet,基于红黑树,查找和插入都具有折半的效率
int n = nums.length;
TreeSet<Long> set = new TreeSet<Long>();
for (int i = 0; i < n; i++) {
Long ceiling = set.ceiling((long) nums[i] - (long) t); // 返回集合中大于或等于nums[i] - t的最小元素
if (ceiling != null && ceiling <= (long) nums[i] + (long) t) {
return true;
}
set.add((long) nums[i]); // 滑动窗口移动
if (i >= k) {
set.remove((long) nums[i - k]);
}
}
return false;
}
}
TreeSet函数:
ceiling(E e) 方法返回在这个集合中大于或者等于给定元素的最小元素,如果不存在这样的元素,返回 null.
floor(E e) 方法返回在这个集合中小于或者等于给定元素的最大元素,如果不存在这样的元素,返回null.
remove(Object O) 删除指定元素
add(Object O) 添加元素
上述解法无法做到线性的原因是:我们需要在大小为 k 的滑动窗口所在的「有序集合」中找到与 u 接近的数
。
如果我们能够将 k 个数字分到 个桶的话,那么我们就能 O(1)
的复杂度确定是否有 [u - t, u + t]
的数字(检查目标桶是否有元素)。
具体的做法为:令桶的大小为 size = t + 1
,根据 u 计算所在桶编号:
[u - t, u + t]
范围的数字,返回 true[u - t, u + t]
范围的数字,如有 返回 true[max(0, i-k), i]
内的桶class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
// 桶排序
long size;
int n = nums.length;
Map<Long, Long> map = new HashMap<>();
size = t + 1L;
for (int i = 0; i < n; i++) {
long u = nums[i] * 1L;
long idx = getIdx(u, size);
// 目标桶已存在(桶不为空),说明前面已有 [u - t, u + t] 范围的数字
if (map.containsKey(idx)) return true;
// 检查相邻的桶
long l = idx - 1, r = idx + 1;
if (map.containsKey(l) && u - map.get(l) <= t) return true;
if (map.containsKey(r) && map.get(r) - u <= t) return true;
// 建立目标桶
map.put(idx, u);
// 移除下标范围不在 [max(0, i - k), i) 内的桶
if (i >= k) map.remove(getIdx(nums[i - k] * 1L, size));
}
return false;
}
long getIdx(long u, long size) {
return u >= 0 ? u / size : ((u + 1) / size) - 1;
}
}
理解 getIdx()
逻辑
为什么 size 需要对 t 进行 +1 操作?
目的是为了确保差值小于等于 t 的数能够落到一个桶中
。
举个
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。