文章目录

  • 滑动窗口算法精讲(Sliding Window Algorithm)
    • 简介
    • 步骤及算法模板
      • 模板1
      • 模板2
    • leetcode例题讲解
      • 入门级
        • 209. 长度最小的子数组
          • 思路:
          • 代码实现
        • 219. 存在重复元素 II
          • 思路
          • 代码实现
        • 220. 存在重复元素 III
          • 思路
          • 代码实现
      • 进阶级
        • 395. 至少有 K 个重复字符的最长子串
          • 思路
          • 代码实现
      • 经典题目
        • 438. 找到字符串中所有字母异位词 (一题三解)
          • 思路1:
          • 代码实现1
          • 思路2
          • 代码实现2
          • 思路3
          • 代码实现3
      • 其它练习题目

滑动窗口算法精讲(Sliding Window Algorithm)

简介

滑动窗口算法的本质是双指针法中的左右指针法,所谓滑动窗口,就像描述的那样,可以理解成是一个会滑动的窗口,每次记录下窗口的状态,再找出符合条件的适合的窗口。它可以将双层嵌套的循环问题,转换为单层遍历的循环问题。使用两个指针一左一右构成一个窗口,就可以将二维循环的问题转化成一维循环一次遍历,相当于通过旧有的计算结果对搜索空间进行剪枝,使时间复杂度从O(n²)降低至O(n),比如经典字符串查找算法Rabin-Karp 指纹字符串查找算法,它本质上也使用了滑动窗口的思想,通过公式推导降低窗口滑动时计算子串哈希值的复杂度。

滑动窗口算法更多的是一种思想或技巧,按照窗口大小是否固定分为固定滑动窗口变长滑动窗口,可以用来解决一些查找满足一定条件连续区间性质(长度等)的问题。往往类似于“请找到满足xx的最x的区间(子串、子数组等)的xx”这类问题都可以使用该方法进行解决。

步骤及算法模板

以下思路是比较形象的滑动窗口问题的解题步骤,但有些题目找到窗口限定比较隐晦,需要具体问题具体分析:

(1)初始化窗口
初始化左右边界 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。

(2)寻找可行解
我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的满足可行解。

(3)优化可行解
此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的可行解不再符合要求。同时,每次增加 left,我们都要更新一轮结果。

(4)滑动窗口,直至一次遍历结束:
重复第 2 和第 3 步,直到 right 到达到的尽头。

这个思路其实也不难理解,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。

模板1

public void slideWindowTemplate(String nums){int l = 0, r = 0;        //[初始化窗口]//codes...               [其他初始化信息,定义一些维护数据的数据结构]while(r < nums.length){//codes.....         [维护窗口中的数据] while(l < r && check(xxx) == false){   //[窗口不满足某种性质]//codes...       [维护窗口中的数据] l++;             //[缩小窗口]}//codes..            [更新结果]r++;                 //[增大窗口]}
}

模板2

public void slideWindowTemplate(String nums){//codes...               [其他初始化信息,定义一些维护数据的数据结构]for(int l = 0, r = 0; r < nums.length; r++){//codes.....         [维护窗口中的数据] while(l < r && check(xxx) == false){   //[窗口不满足某种性质]//codes...       [维护窗口中的数据] l++;             //[缩小窗口]}//codes..            [更新结果]}
}

模板1和模板2算法本质是一样的,只是实现不同,读者可根据爱好来选择。

leetcode例题讲解

例题分为三种级别,一是入门级题目,二是进阶题目,三是经典题目分别挑选一道题进行讲解。

入门级

209. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组[numsl,numsl+1,...,numsr−1,numsr][nums_l, nums_{l+1}, ..., nums_{r-1}, nums_r] [numsl​,numsl+1​,...,numsr−1​,numsr​],并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:

输入:target = 4, nums = [1,4,4]
输出:1
示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

地址:209. 长度最小的子数组

思路:

本题为变长的滑动窗口,其窗口大小由target决定(窗口总和大于等于target),所以定义一个变量(窗口内总和),小于target右指针右移,大于target左指针右移,等于记录更新窗口大小即可

代码实现
package com.algorithm.num209;/*** 思路:滑动窗口实现,定义一个变量(窗口内总和),* 小于target右指针右移,大于target左指针右移,等于记录更新窗口大小**  @author hh*  @date 2022-2-3 20:10*/
public class Solution {public int minSubArrayLen(int target, int[] nums) {int result = Integer.MAX_VALUE;int numsLen = nums.length;int winNums = 0;//双指针l,rfor(int l = 0,r = 0; r < numsLen; r++){winNums += nums[r];//如果区间和大于target左指针右移while (winNums >= target){//先更新长度result = Math.min(result,r - l + 1);//缩小左窗口winNums -= nums[l];l++;}}return result == Integer.MAX_VALUE ? 0 : result;}
}

219. 存在重复元素 II

给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。

示例 1:

输入:nums = [1,2,3,1], k = 3
输出:true
示例 2:

输入:nums = [1,0,1,1], k = 1
输出:true
示例 3:

输入:nums = [1,2,3,1,2,3], k = 2
输出:false

地址:219. 存在重复元素 II

思路

滑动窗口,窗口最大为k,其中r - l <= k,把窗口中的元素保存到集合中,每次左右指针移动更新集合即可,如果集合中存在元素则返回true

代码实现
package com.algorithm.num219;import java.util.LinkedHashSet;
import java.util.Set;/*** 思路:滑动窗口,窗口最大为k,其中r - l <= k,把窗口中的元素保存到集合中,每次* 左右指针移动更新集合即可,如果集合中存在元素则返回true**  @author hh*  @date 2022-2-3 21:18*/
public class Solution {public boolean containsNearbyDuplicate(int[] nums, int k) {int numLength = nums.length;Set<Integer> set = new LinkedHashSet<>();for(int l = 0, r = 0; r < numLength; r++){//判断集合中是否存在,存在则直接返回true,没有则添加到集合中if(set.contains(nums[r])){return true;}set.add(nums[r]);//如果窗口大小则缩小窗口while (r - l >= k){set.remove(nums[l]);l++;}}return false;}
}

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

地址: 220. 存在重复元素 III

思路

本题与219题目非常类型,只不过条件有了变化。所以可以选取数据结构TreeSet来实现。使用TreeSet保存窗口中元素,窗口扩大时需要判断有没有大于等于nums[r] - t的元素,如果有,则返回为true,当达到窗口大小缩小窗口,并移除窗口中的元素即可

代码实现
package com.algorithm.num220;import java.util.TreeSet;/*** 思路:滑动窗口,使用TreeSet保存窗口中元素,窗口扩大时需要判断有没有大于等于nums[r] - t的元素,* 如果有,则返回为true,当达到窗口大小缩小窗口,并移除窗口中的元素即可**  @author hh*  @date 2022-2-3 21:44*/
public class Solution {public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {int numsLength = nums.length;TreeSet<Long> treeSet = new TreeSet<>();for(int l = 0,r = 0; r < numsLength; r++){long down = (long) nums[r] - t;long up = (long)nums[r] + t;//同时满足上下限Long cellingVal = treeSet.ceiling(down);Long floorVal = treeSet.floor(up);if((cellingVal != null && cellingVal <= nums[r]) || (floorVal != null && floorVal >= nums[r])){return true;}treeSet.add((long)nums[r]);//是否到达窗口的最大while (r - l >= k){treeSet.remove((long)nums[l]);l++;}}return false;}
}

进阶级

395. 至少有 K 个重复字符的最长子串

给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。

示例 1:

输入:s = “aaabb”, k = 3
输出:3
解释:最长子串为 “aaa” ,其中 ‘a’ 重复了 3 次。
示例 2:

输入:s = “ababbc”, k = 2
输出:5
解释:最长子串为 “ababb” ,其中 ‘a’ 重复了 2 次, ‘b’ 重复了 3 次。

地址:395. 至少有 K 个重复字符的最长子串

思路

题目说明了只包含小写字母(26 个,为有限数据),我们可以枚举最大长度所包含的字符类型数量,答案必然是 [1, 26],即最少包含 1 个字母,最多包含 26 个字母。

你会发现,当确定了长度所包含的字符种类数量时,区间重新具有了二段性质。

当我们使用双指针的时候:

  • 右端点往右移动必然会导致字符类型数量增加(或不变)

  • 左端点往右移动必然会导致字符类型数量减少(或不变)

当然,我们还需要记录有多少字符符合要求(出现次数不少于 k),当区间内所有字符都符合时更新答案。

代码实现
package com.algorithm.num395;/*** 思路:滑动窗口,但本题关键是无法确定窗口的大小,由于字母仅限小写英文字母,所以我们可以枚举窗口中字母类型数量,* 从1到26,这样就限定了窗口的大小,从而解决问题。定义一个变量winCharTypeNums保存当前窗口的字母类型数量,当枚举* 字母类型数量大于winCharTypeNums时扩大窗口,等于时更新最大长度,小于时缩小窗口。保存字母数量使用数组保存,* 扩大或缩小时更新数组**  @author hh*  @date 2022-2-4 11:16*/
public class Solution {/*** 记录满足条件的最大长度*/private int maxLength = Integer.MIN_VALUE;public int longestSubstring(String s, int k) {int strLength = s.length();for(int enumNum = 1; enumNum <= 26; enumNum ++){//定义变量int winCharTypeNums = 0;int[] frequency = new int[26];for(int l = 0,r = 0; r < strLength; r++){//判断之前数组是否记录,没有记录则winCharTypeNums加1,否则只更新数组int rightCharIndex = s.charAt(r) - 'a';if(frequency[rightCharIndex] == 0){winCharTypeNums++;}frequency[rightCharIndex] ++;//检查更新满足条件的最大长度if(winCharTypeNums == enumNum){check(frequency,k);}//检测是否需要缩小窗口while (winCharTypeNums >  enumNum){//获取左边界字符,判断是否需要更新winCharTypeNumsint leftCharIndex = s.charAt(l) - 'a';if(frequency[leftCharIndex] == 1){winCharTypeNums--;}frequency[leftCharIndex]--;l++;}}}return maxLength == Integer.MIN_VALUE ? 0 : maxLength;}private void check(int[] frequency,int k){int strLength = 0;for (int value : frequency) {if (value != 0 && value < k) {return;}strLength += value;}maxLength = Math.max(strLength,maxLength);}}

经典题目

438. 找到字符串中所有字母异位词 (一题三解)

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:

输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的异位词。
示例 2:

输入: s = “abab”, p = “ab”
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的异位词。

地址:438. 找到字符串中所有字母异位词

思路1:

根据题目要求,我们需要在字符串 s 寻找字符串 p的异位词。因为字符串 p 的异位词的长度一定与字符串 p 的长度相同,所以我们可以在字符串 s 中构造一个长度为与字符串 p 的长度相同的滑动窗口,并在滑动中维护窗口中每种字母的数量;当窗口中每种字母的数量与字符串 p 中每种字母的数量相同时,则说明当前窗口为字符串 p 的异位词

代码实现1
package com.algorithm.num438;import java.util.LinkedList;
import java.util.List;/*** 思路:滑动窗口,窗口大小固定,每次滑动时需比较是否为异位词**  @author hh*  @date 2022-2-4 17:39*/
public class Solution {public List<Integer> findAnagrams(String s, String p) {List<Integer> result = new LinkedList<>();int[] pat = new int[26];int[] text = new int[26];int strLength = s.length();for(char c : p.toCharArray()){pat[c - 'a']++;}for(int l = 0,r = 0; r < strLength; r++){text[s.charAt(r) - 'a'] ++;if(l + p.length() - 1 == r && this.isAnagrams(text,pat)){//检测是否为异位词result.add(l);}while (l + p.length() - 1 <= r){text[s.charAt(l) - 'a']--;l++;}}return result;}private boolean isAnagrams(int[] text,int[] pat){for(int i = 0; i < text.length; i++){if(text[i] != pat[i]){return false;}}return true;}
}
思路2

在方法一的基础上,我们不再分别统计滑动窗口和字符串 p 中每种字母的数量,而是统计滑动窗口和字符串 p中每种类型字母的差;并引入变量differ 来记录当前窗口与字符串 p 中数量不同类型的字母的,并在滑动窗口的过程中维护它。

在判断滑动窗口中每种字母的数量与字符串 p 中每种字母的数量是否相同时,只需要判断 differ 是否为零即可。

代码实现2
package com.algorithm.num438;import java.util.LinkedList;
import java.util.List;/***  @author hh*  @date 2022-2-4 21:20*/
public class Solution2 {public List<Integer> findAnagrams(String s, String p) {List<Integer> result = new LinkedList<>();if(s.length() < p.length()){return result;}//维护窗口的差值int[] diffArr = new int[26];//diff表示字符类型不同的数量int diff = 0;//预处理for(int i = 0;  i< p.length();i++){diffArr[p.charAt(i) - 'a']++;diffArr[s.charAt(i) - 'a']--;}for(int i  = 0 ; i < 26; i++ ){if(diffArr[i] != 0){diff++;}}if(diff == 0){result.add(0);}for(int l = 0; l < s.length() - p.length(); l++){int leftIndex = s.charAt(l) - 'a';if(diffArr[leftIndex] == 0){diff++;}else if(diffArr[leftIndex] == -1){diff--;}diffArr[leftIndex]++;int rightIndex = s.charAt(l + p.length()) - 'a';if(diffArr[rightIndex] == 0){diff++;}else if(diffArr[rightIndex] == 1){diff--;}diffArr[rightIndex]--;if(diff == 0){result.add(l+1);}}return result;}
}
思路3

与思路1类似,本思路记录窗口中字母类型数量及词频和字符串p的字母类型数量及词频是否相等。

解法一中每次对滑动窗口的检查都不可避免需要检查两个词频数组,复杂度为 O(C)O(C)O(C)。

事实上,我们只关心两个数组是否完全一致,因而我们能够只维护一个词频数组 cnt 来实现。

起始处理 p 串时,只对 cnt 进行词频字符自增操作。当处理 s 的滑动窗口子串时,尝试对 cnt 中的词频进行「抵消/恢复」操作:

  • 当滑动窗口的右端点右移时(增加字符),对 cnt 执行右端点字符的「抵消」操作;

  • 当滑动窗口的左端点右移时(减少字符),对 cnt 执行左端点字符的「恢复」操作。

同时,使用变量 a统计 p 中不同字符的数量,使用变量 b统计滑动窗口(子串)内有多少个字符词频与 p 相等。

当滑动窗口移动( 执行「抵消/恢复」)时,如果「抵消」后该字符词频为 0,说明本次右端点右移,多产生了一位词频相同的字符;如果「恢复」后该字符词频数量为 1,说明少了一个为词频相同的字符。当且仅当 a = b 时,我们找到了一个新的异位组。

代码实现3
package com.algorithm.num438;import java.util.LinkedList;
import java.util.List;/***  @author hh*  @date 2022-2-4 21:20*/
public class Solution3 {public List<Integer> findAnagrams(String s, String p) {List<Integer> result = new LinkedList<>();//维护窗口的差值int[] diffArr = new int[26];int strLength = s.length();int a = 0,b = 0;//预处理for(char c : p.toCharArray()){diffArr[c - 'a']++;}for(int i : diffArr){if(i > 0){a++;}}for(int l = 0,r = 0; r < strLength; r++){int rightIndex = s.charAt(r) - 'a';if(diffArr[rightIndex] == 1){//由不同变为相同b++;}diffArr[rightIndex]--;if(l + p.length() - 1 == r && a == b){//检测是否为异位词result.add(l);}while (l + p.length() - 1 <= r){int leftIndex = s.charAt(l) - 'a';if(diffArr [leftIndex] == 0){b--;}diffArr[leftIndex]++;l++;}}return result;}
}

其它练习题目

请读者自行练习

  • 入门:424. 替换后的最长重复字符
  • 经典:567. 字符串的排列

滑动窗口算法精讲(Sliding Window Algorithm)相关推荐

  1. ST算法 Sliding Window algorithm template

    ST算法(Sliding Window):A easy way to slove the substring problems algorithm template to slove substrin ...

  2. 滑动窗口算法(Sliding Window Algorithm)

    目录 滑动窗口算法(Sliding Window Algorithm) 一.基本示例 二.经典使用 1. 连续元素最大和 题目 解法及分析 2. 最长无重复子字符串 题目 解法及分析 三.参考资料 滑 ...

  3. Java算法-滑动窗口算法(Sliding Window)(十)

    滑动窗口 滑动窗口概念不仅存在于数据链路层,也存在于传输层,两者有不同的协议,但基本原理是相近的.其中一个重要区别是,一个是针对于帧的传送,另一个是字节数据的传送. 滑动窗口(Sliding wind ...

  4. 从零开始学习VIO笔记 --- 第四讲:滑动窗口(基于滑动窗口算法的 VIO 系统:可观性和一致性)

    从零开始学习VIO笔记 --- 第四讲:滑动窗口(基于滑动窗口算法的 VIO 系统:可观性和一致性) 一. 从高斯分布到信息矩阵 1.1 高斯分布 1.2 高斯分布和协方差矩阵 1.3 信息矩阵 二. ...

  5. c语言 滑窗法_滑动窗口算法(一)

    某日事不多,点开sentinel-core代码学习,想看看qps.rt等是怎么统计的. 点开StatisticSlot类,发现里面是用DefaultNode增加qps,然后尝试点开 DefaultNo ...

  6. 滑动窗口算法基本原理与实践

    原文作者:huansky 原文地址:滑动窗口算法基本原理与实践 目录 滑动窗口算法(Sliding Window Algorithm) 基本示例 滑动窗口法的大体框架 算法实例 1208. 尽可能使字 ...

  7. Java实现滑动窗口算法

    Java实现滑动窗口算法 滑动窗口算法 滑动窗口应用实例 基本实例 断路器怎么识别断路器的状态 代码实现 滑动窗口算法 思想不滑坡 方法总比困难多 滑动窗口协议(Sliding Window Prot ...

  8. 列表左右箭头滑动_我写了一套框架,把滑动窗口算法变成了默写题

    读完本文,你可以去力扣拿下如下题目: 76.最小覆盖子串 567.字符串的排列 438.找到字符串中所有字母异位词 3.无重复字符的最长子串 ----------- 鉴于前文 二分搜索框架详解 的那首 ...

  9. 滑动窗口算法通用思想

    文章目录 一.最小覆盖子串 二.找到字符串中所有字母异位词 三.无重复字符的最长子串 最后总结 本文详解「滑动窗口」这种高级双指针技巧的算法框架,带你秒杀几道难度较大的子字符串匹配问题: 最小覆盖子串 ...

最新文章

  1. B-监狱逃脱(BFS)
  2. C语言中长度为0的数组
  3. Go gin获取GET请求参数
  4. java程序cpu突然飚高_Java 定位导致CPU飙升的代码过程
  5. AtomicInteger 的使用
  6. 华为P50系列确定29日发布:但遗憾的是...
  7. dlut-KFQ概率上机2
  8. VoIP之语音编码器
  9. NopCommerce支持友好路由
  10. 深度学习自学(二十六):人脸数据集
  11. 深度学习中网络在训练时不收敛的原因
  12. tomcat重启时出现:严重: Error filterStart
  13. 聊聊新西兰的工作和生活
  14. Python中的Counter类
  15. 关闭 C4996 警告(_CRT_SECURE_NO_DEPRECATE)方法
  16. matplotlib中文字体设置
  17. java程序模拟手机
  18. 祝大家2022幸福安康
  19. oppo手机刷linux,Ubuntu手机系统介绍及Ubuntu刷机教程分享
  20. SwiftUI Mapkit 导航基础教程大全之 MKMapView地图显示并实现导航线路(教程含源码)

热门文章

  1. 【LDA】LDA主题模型笔记—主要是狄利克雷
  2. 2、fixture机制
  3. linux服务器查看公网IP信息的方法
  4. android仿百度外卖波浪_iOS实现百度外卖头像波浪的效果
  5. word另存为html emz和wmz,轉載:如何將offcie 2003文檔(.doc、.xls、.ppt)轉換成mht文檔...
  6. VxWorks 操作系统简介
  7. echarts 之常用的属性配置
  8. java 日志的作用_java日志概述和原理
  9. MATLAB自带工具箱实现PCA降维代码
  10. 行业分析-全球与中国电源和充电器市场现状及未来发展趋势