技巧解释:
滑动窗口技巧其实是双指针算法合理运用的一种技巧,即通过两个指针的移动,一个指针在前一个在后,动态处理一组数据以便达到简化暴力法的目的,这也说明一般滑动窗口技巧是部分题目暴力法的优化方法。。
图示:
例题:
例题1:209.长度最小的数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
双指针技巧分析:
由于题目要求我们找出和大于target的长度最小子数组,我们可以先定义两个指针指向数组的头部,再定义一个结果变量(result)此处我们初始化为:nums.length + 1,再定义求和变量total;先让j指针移动,total累加求和,当total的值大于等于target时让total减去nums[ i ],每次取result和j-i的最小值,此题目中就是通过j-i来抽象形成一个滑动窗口
代码:
public int minSubArrayLen(int target, int[] nums) {
//Sliding Window | Two Pointer
//N is the size of nums
//Time complexity: O(N)
//Space complexity: O(1)
if (nums == null || nums.length == 0) {
return 0;
}
//定义双指针
int i = 0;
int j = 0;
int result = nums.length + 1;
int total = 0;
while (j < nums.length) {
total += nums[j];
j++;
//当长度大于等于target时,开始滑动窗口
while (total >= target) {
result = Math.min(result,j - i);
//将i处值“滑出”“窗口”
total -= nums[i];
//i++窗口缩小
i++;
}
return result == nums.length + 1 ? 0 : result;
}
总结:对于本题目的“滑动窗口” 算法,最重要的两点就是:1.如何动态维护滑动的窗口;2.如何动态更新题目需求结果!!!对于这两点个人感觉是判断是否能使用“滑动窗口”技巧的关键,也是运用滑动窗口解题的关键,有时我们为了更好的完成以上两点,我们还可以借助一些数据结构的特点实现两点!!!(我们以219。存在重复元素II为例子)
例题2:219.存在重复元素II
给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。
双指针技巧分析:
有了第一个例题总结出来的两个关键待解决点,我们针对那两点分别指出解决方法,1.我们以每个长度为k的子数组为考虑对象(即每次动态维护“滑动窗口”的长度为k)那么本题目该如何动态维护其长度为k呢?我们从数组的基地址开始遍历,当遍历的索引(假设索引为i)大于k时我们通过“i - k - 1”每次移除窗口中的最后一位。2.对于本题目由于只需要判断是否存在,及我们只要判断在长度为k的子数组中,判断是否存在最近的两个符合题目的元素,那么我们可以选用java中HashSet相同后一个元素添加会失败,每次遍历元素时如果不存在则添加到HashSet中,如果添加失败则返回true。(这一点就正如前面一题所总结,我们可以选择合理的数据结构帮助完成元素的比较和相关操作!)
图示:
代码:
public boolean containsNearbyDuplicate(int[] nums, int k) {
HashSet<Integer> set = new HashSet<>();
for (int i = 0; i < nums.lengt; i++) {
//动态维护“滑动窗口”
if (i > k) {
set.remove(nums[i-k-1]);
}
//如果添加失败
//说明在“窗口”中存
if (!set.add(i)) {
return true;
}
}
return false;
}
补充:此题目也可以用另一个集合HashMap的特性(相同的后一个元素会覆盖前一个元素)来解答,但不属于“滑动窗口”解法(但也可以看作存在其思想)。
public boolean containsNearbyDuplicate(int[] nums, int k) {
//HashMap
//Time Complexiy: O(N)
//Space Complexity: O(N)
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
//定义辅助变量,用于对子区间内的元素比较
int num = nums[i];
//如果map中已经存在值相同元素
//并且索引之差小于等于k
if (map.containsKey(num) && i - map.get(num) <= k) {
return true;
}
map.put(num,i);
}
return false;
}
总结:做完本题目和上一个题目,我们可以发现在施行维护“滑动窗口”之前,我们其实都是先完成了一个先生成一个符合题意的“窗口”的抽象操作,对于209题,我们利用变量“total”达到大于等于“target”时记录下来的下标j和初始另一个下标i形成窗口;对于219题我们先遍历到长度为k的子数组,其后在对长度k维护!!!(接下来的一题对于该操作体现的更加明显)
例题3:643子数组最大平均数I
给你一个由 n 个元素组成的整数数组 nums 和一个整数 k 。
请你找出平均数最大且 长度为 k 的连续子数组,并输出该最大平均数。
任何误差小于 10-5 的答案都将被视为正确答案。
双指针技巧分析:
题目需要我们找出找出平均数最大且 长度为 k 的连续子数组,那么我们只需要找出和最大,长度为k的子数组即可,为了使用双指针技巧,以及比较元素得出最大和长度为k的 子数组,我们进行如下操作:1.先将遍历到下标为k-1的位置得到“定长窗口”;2.再从下标为k的位置开始遍历,实现j - k完成对“窗口”的维护,并每次比较子数组中的元素和,得出最大和!!!
代码:
public double findMaxAverage(int[] nums, int k) {
int sum = 0;
//先遍历到下标为k-1的位置
//生成“窗口”
for (int i = 0; i < k; i++) {
sum += num[i];
}
//定义最大结果边练
//用于每轮维护的比较
int maxSum = sum;
//再从k位置开始遍历
//每次实现维护窗口为定长
for (int j = k; j < nums.length; j++) {
sum = sum + nums[j] - nums[j-k];
maxSum = Math.max(sum,Maxsum);
}
//返回结果
//注意要返回double类型
return 1.0 * maxSum / k;
}
总结:此题目就比较明显的体现出,先生成窗口的操作。
最后总结:
对于“滑动窗口”技巧我们该明白如下几点:
1.其为部分题解暴力法的优化方法
2.实际上是双指针算法的合理运用
2.用该技巧解题关键在于如何先生成窗口;如何依据题意维护窗口;如何对窗口内元素进行操作
(个人认为,此处虽然只有三个题目为例(不排除特殊情况)还不能够将滑动窗口的通解逻辑思想体现出来,但是从逻辑的角度看若要维护窗口,那就必须先存在窗口,对于具体问题,还需要具体分析!!!)