滑动窗口算法系列|基础概念|例题讲解

大家好,我是LvZi,今天带来滑动窗口算法系列|基础概念|例题讲解
在这里插入图片描述

一.滑动窗口问题基础概念

滑动窗口本质上是同向双指针问题,脱胎于双指针.使用两个指针l, r维护一定长度的数组区间,在r 指针遍历的过程中,执行进窗口,判断,更新结果,出窗口 等操作,当r指针遍历完毕,就能得到最后的结果

滑动窗口算法的代码比较固定,大致是以下步骤:

  1. 进窗口 将元素添加到区间内部 可以使用变量,数组,哈希表维护
  2. 判断 判断添加元素之后,当前区间是否满足要求;如果满足执行出窗口操作
  3. 更新结果 虽然放到第三步,但是更新结果的时机要因题而异

滑动窗口之所以快,是因为实现了一次遍历得到结果,减少了暴力循环带来的冗余操作

1.基本概念

滑动窗口算法是一种高效的算法,用于解决涉及连续子数组或子字符串的问题。它通过维护一个动态窗口来扫描数组或字符串,从而减少重复计算,提高算法效率。这个动态窗口根据性质可以分为两类:

  1. 固定大小的滑动窗口
  2. 可变大小的滑动窗口

2.固定大小的滑动窗口

在固定大小的滑动窗口中,窗口的大小是预先确定的,窗口从左到右逐个滑动。常见问题包括:

  • 最大子数组和(大小固定):找到一个大小为k的子数组,使其和最大。
  • 平均子数组和(大小固定):找到一个大小为k的子数组,使其和的平均值最大。

3.可变大小的滑动窗口

在可变大小的滑动窗口中,窗口的大小是动态变化的,取决于具体问题。常见问题包括:

  • 最小覆盖子串:找到包含所有给定字符的最小子串。
  • 最长无重复字符子串:找到没有重复字符的最长子串。

4.技巧和策略

  • 双指针技术:使用两个指针(l,r),一个指向窗口的起始位置,一个指向窗口的结束位置,以便动态调整窗口大小
  • 哈希表/字典:常用于记录窗口内元素的频率,帮助快速检查条件(例如字符是否满足要求)。也经常会使用数组模拟哈希表
  • 条件判断和滑动窗口的调整:根据问题的要求,动态调整窗口的大小和位置。
  • 其实也不用纠结使用定长还是不定长的,只要分析出题目是使用滑动窗口解决就行;窗口的定长还是不定长影响的是更新结果的时机,而这个时机根据具体题目具体判断即可
  • 还有最重要的一点是:判断是否能使用滑动窗口(同向双指针)的关键点在于数组是否具有单调性,注意不是数组元素严格的单增单减,要结合题目所求

二.例题讲解

1.⻓度最⼩的⼦数组

⻓度最⼩的⼦数组
在这里插入图片描述

分析

  • 最简单的方法就是暴力查找,但是会超时

滑动窗口解法

  1. 进窗口 使用sum来维护区间和 遍历到一个数字就加
  2. 判断 判断sum是否大于等于target 如果成立 更新结果 +出窗口
  • 本题的单调性在于:数组中的元素都是正数,随着指针的移动,和一定是越来越大的

在这里插入图片描述

代码

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length, sum = 0, len = 0x3f3f3f3f;
        for(int i = 0, j = 0; j < n; j++) {
            sum += nums[j];// 进窗口
            while(sum >= target) {// 判断
            	// 判断成立  更新结果 + 出窗口
                len = Math.min(len, j - i + 1);
                sum -= nums[i++];
            }
        }

        return len == 0x3f3f3f3f ? 0 : len;
    }
}

2.⽆重复字符的最⻓⼦串

⽆重复字符的最⻓⼦串
在这里插入图片描述
分析

● 经典的滑动窗口问题,本题的一个技巧在于使用数组模拟哈希表

  1. 进窗口 添加字符 字符数组记录字符出现的次数
  2. 判断 判断添加的字符的出现次数是否>2,如果大于则是重复字符,出窗口
  3. 更新结果 每次添加进字符就更新一次结果

s的ASCII码范围是0-128,所以可以使用大小为128的数组模拟哈希表
代码

class Solution {
    public int lengthOfLongestSubstring(String ss) {
        int[] hash = new int[128];
        char[] s = ss.toCharArray();
        int slow = 0, fast = 0, n = s.length, len = -1;
        if(n == 0) return 0;
        while(fast < n) {
            ++hash[s[fast]];// 进窗口
            while(hash[s[fast]] > 1)// 判断
                hash[s[slow++]]--;// 判断成立  出窗口
            
            len = Math.max(fast - slow + 1, len);// 更新结果
            fast++;
        }

        return len;
    }
}

3.最⼤连续 1 的个数 III

最⼤连续 1 的个数 III
在这里插入图片描述
分析

  • 采用转化思想,如果考虑翻转/改变数组,比较麻烦,可以转化为统计区间内部0的个数,只要保证区间内部0的个数不超过k,就一定能翻转成功

滑动窗口思路

  1. 进窗口 使用二进制数字统计当前数字出现的次数
  2. 判断 判断0出现的次数是否超过k,如果超过,出窗口
  3. 更新结果 每遍历到一个数字就更新一次结果

代码

  • 方法一:使用计数器统计0的数量
class Solution {
    public int longestOnes(int[] nums, int kk) {
        // 使用计数器统计数量  有点抽象
        int slow = 0, fast = 0, ret = -1, n = nums.length, k = kk;
        while(fast < n) {
            if(nums[fast] == 0) k--;
            while(k < 0) {
                if(nums[slow++] == 0) k++;
            }

            ret = Math.max(ret, fast - slow + 1);
            fast++;
        }
        return ret;
    }
}
  • 方法二:使用二进制数组
class Solution {
    public int longestOnes(int[] nums, int k) {
        // 二进制数组  0下标存储0出现的次数  1下标存储1出现的次数
        int[] arr = new int[2];
        int slow = 0, fast = 0, ret = -1, n = nums.length;
        while(fast < n) {
            arr[nums[fast]]++;
            while(arr[0] > k)
                if(nums[slow++] == 0)
                    arr[0]--;
            ret = Math.max(ret, fast - slow + 1);
            ++fast;
        }
        return ret;
    }
}

4.将 x 减到 0 的最⼩操作数

将 x 减到 0 的最⼩操作数

在这里插入图片描述

分析

  • 从左右两端选择最少个数的数字,使得和恰好等于x
  • 转化为:从数组中挑选连续区间的数字,使得和恰好等于target的最长的区间.和第一题类似,这里求的是满足条件下的最长的子数组

代码

class Solution {
    public int minOperations(int[] nums, int x) {
        int sum = 0;
        for(int n : nums) sum += n;
        int target = sum - x;
        if(target < 0) return -1;// nums全是正数

        int l = 0, r = 0, tmp = 0, n = nums.length, ret = -1;
        while(r < n) {
            tmp += nums[r];// 进窗口

            while(tmp > target) {// 判断
                tmp -= nums[l++];// 判断成立 出窗口
            }

            if(tmp == target)// 更新结果
                ret = Math.max(ret, r - l + 1);
            ++r;
        }

        if(ret == -1) return -1;
        return n - ret;
    }
}

5.水果成篮

水果成篮
在这里插入图片描述
分析
使用kinds记录区间内部苹果种类的个数

  1. 进窗口 增加对应种类苹果的数量 如果是新种类,kinds++;
  2. 判断 判断kinds > 2,如果大于,出窗口
  3. 更新结果

代码

class Solution {
    public int totalFruit(int[] fruits) {
        // 从左往右找  满足只有两个种类苹果的最大数目  滑动窗口
        int l = 0, r = 0, n = fruits.length, ret = -1, kinds = 0;
        int[] hash = new int[n + 1];// 统计种类的数目

        while(r < n) {
            if(hash[fruits[r]] == 0) ++kinds;// 判断是否是新种类
            hash[fruits[r]]++;// 进窗口

            // 判断
            while(kinds > 2) {
                hash[fruits[l]]--;
                if(hash[fruits[l]] == 0) kinds--;// 数量为0  种类减1
                ++l;
            }

            // 更新结果
            ret = Math.max(ret, r - l + 1);
            ++r;
        }

        return ret;
    }
}

6.找到字符串中所有字⺟异位词(固定窗口大小)

找到字符串中所有字⺟异位词

在这里插入图片描述
分析

  • 难点在于如何判断两个指针区间的字符串和p字符串是否满足异位词
  • 如果是异位词,则字母类型及个数完全相等,可以考虑使用两个哈希表记录字母及出现的频数,但是要求窗口内部的字符必须都在p中 ,所以使用cnt来记录有效字符的个数
  • 由于都是小写字母,考虑使用数组模拟哈希表,通过数组记录字母出现的频数
  • 本题是一个固定大小的滑动窗口问题,大小等于字符串p的长度,应该保证窗口的大小始终不标
  1. 进窗口 将对应字符的频数加1,并判断是否是有效字符
  2. 判断 判断当前区间大小是否等于p的长度,如果满足,出窗口
  3. 更新结果 判断有效字符的个数是否等于p的长度

代码

class Solution {
    public List<Integer> findAnagrams(String ss, String pp) {
        // 滑动窗口算法
        char[] s = ss.toCharArray(), p = pp.toCharArray();
        int l = 0, r = 0, n = s.length;
        int[] hash1 = new int[26], hash2 = new int[26];
        for(char ch : p) hash2[ch - 'a']++;
        List<Integer> ret = new ArrayList<>();

        int cnt = 0;// 使用cnt统计有效字符的数量
        while(r < n) {
            // 进窗口
            char in = (char)(s[r] - 'a');
            hash1[in]++;
            if(hash1[in] <= hash2[in]) cnt++;// 有效字符

            // 判断 + 出窗口
            if(r - l + 1 > p.length) {
                char out = (char)(s[l] - 'a');
                if(hash1[out] <= hash2[out]) cnt--;
                hash1[out]--;
                ++l;
            }

            // 更新结果
            if(cnt == p.length) ret.add(l);
            ++r;
        }

        return ret;
    }
}

总结
本题判断是否是异位词的策略很巧妙,即使用一个计数变量cnt来标记有效字符的个数,判断区间内部有效字符的个数和字符串p是否相等来判断是否是异位词,此外还有两个比较繁琐的判断策略,这里也提供给大家

  1. 使用hash2统计字符串p中的所有字符及其出现的频率,使用hash1来统计遍历过程中的字符和出现的频率,当区间长度相等时,判断两个哈希表是否相等即可(equals())
  2. 使用数组模拟哈希表,具体策略和1类似,在判断是否相等时可使用循环遍历判断两个数组是否相等

7.串联所有单词的⼦串(分组 + 滑动窗口)

串联所有单词的⼦串
在这里插入图片描述
分析

  • 本题是字母异位词的plus版本,字母异位词中需要判断是否含有某个字符,再遍历的过程中是一个字符一个字符进行判断,本题需要判断的是字符串
  • 算法的思路大致和字母异位词相等,有三点需要注意
  1. 滑动窗口的执行次数:在字母异位词这道题目中,执行一次滑动窗口就能完成,因为是按字符遍历,但是本题是按字符串遍历,需要找到字符串起始字符的位置,有效字符的起始位置不一定就是0位置,也有可能是1,2,…位置,但是最多等于words[i].length - 1,所以需要执行words[i].length - 1次滑动窗口算法
  2. 哈希表的存储:字母异位词中使用哈希数组模拟哈希表,因为都是小写的字符;本题只能使用哈希表还存储字符串和其出现的频率
  3. l和r指针的移动步数:本题是按字符串遍历,所以指针一次移动的步数等于words[i].length - 1

代码

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> ret = new ArrayList<>();
        int m = words.length, n = words[0].length(), len = s.length();
        if(len < m * n) return ret;

        Map<String, Integer> hash1 = new HashMap<>();
        for(String str : words) hash1.put(str, hash1.getOrDefault(str, 0) + 1);

        for(int i = 0; i < n; i++) {
            int l = i, r = i, cnt = 0;
            Map<String, Integer> hash2 = new HashMap<>();
            while(r + n <= len) {// 等于len的时候也有可能满足条件  下面唯一会发生的越界的就是第一行代码  是左闭右开
                // 进窗口
                String in = s.substring(r, r + n);
                hash2.put(in, hash2.getOrDefault(in, 0) + 1);
                if(hash2.get(in) <= hash1.getOrDefault(in, 0)) cnt++;// 有效字符

                // 判断 + 出窗口
                if(r - l + 1 > m * n) {
                    String out = s.substring(l, l + n);
                    if(hash2.get(out) <= hash1.getOrDefault(out, 0)) cnt--;// 删除的是有效字符
                    hash2.put(out, hash2.get(out) - 1);
                    l += n;
                }

                // 更新结果
                if(cnt == m) ret.add(l);
                r += n;
            }
        }

        return ret;
    }
}

8.最小覆盖子串

最小覆盖子串
在这里插入图片描述
分析

  • 本题的解法思路和上题类似
  • 需要注意本题求的是最小覆盖子串,求的是最小长度,需要在条件判断成立是更新结果

代码
哈希表解法

class Solution {
    public String minWindow(String ss, String tt) {
        char[] s = ss.toCharArray(), t = tt.toCharArray();
        Map<Character, Integer> hash1 = new HashMap<>();
        Map<Character, Integer> hash2 = new HashMap<>();
        for(char ch : t) hash2.put(ch, hash2.getOrDefault(ch, 0) + 1);

        String ret = "";
        int l = 0, r = 0, n = s.length, len = t.length, minlen = 0x3f3f3f3f, cnt = 0;
        if(n < len) return ret;

        while(r < n) {
            // 进窗口
            char in = s[r];
            hash1.put(in, hash1.getOrDefault(in, 0) + 1);
            if(hash1.get(in) <= hash2.getOrDefault(in, 0)) cnt++;

            // 判断 + 出窗口 + 更新结果
            while(cnt == len) {
                if(r - l + 1 < minlen) {
                    ret = ss.substring(l, r + 1);
                    minlen = r - l + 1;
                }
                char out = s[l];
                if(hash1.get(out) <= hash2.getOrDefault(out, 0)) cnt--;
                hash1.put(out, hash1.get(out) - 1);
                ++l;
            }

            ++r;
        }

        return ret;
    }
}

数组解法

  • s和t中的元素都是英文字母,ASCII码值为97-122,可以开辟一个大小为128的数组(128是ASCII码的最大值)
class Solution {
    public String minWindow(String ss, String tt) {
        char[] s = ss.toCharArray(), t = tt.toCharArray();
        int[] hash1 = new int[128], hash2 = new int[128];
        for(char ch : t) ++hash2[ch];

        String ret = "";
        int l = 0, r = 0, n = s.length, len = t.length, minlen = 0x3f3f3f3f, cnt = 0;
        if(n < len) return ret;

        while(r < n) {
            // 进窗口
            ++hash1[s[r]];
            if(hash1[s[r]] <= hash2[s[r]]) cnt++;

            // 判断 + 出窗口 + 更新结果
            while(cnt == len) {
                if(r - l + 1 < minlen) {
                    ret = ss.substring(l, r + 1);
                    minlen = r - l + 1;
                }
                if(hash1[s[l]] <= hash2[s[l]]) cnt--;
                hash1[s[l++]]--;
            }

            ++r;
        }

        return ret;
    }
}
  • 所谓的算法优化都是建立在暴力解法的基础之上,正是看到了暴力解法的冗余,才想到优化的算法

  • 滑动窗口算法有一个比较明显的切入点求区间内部最长/最短问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/763037.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Study--Oracle-05-Oracler体系结构

一、oracle 体系概览 Oracle数据库的体系结构通常包括以下主要组件&#xff1a; 1、实例&#xff08;Instance&#xff09;&#xff1a;运行数据库的软件环境&#xff0c;包括内存结构&#xff08;SGA&#xff09;和进程结构&#xff08;Background Processes and User Proces…

如何一键修复0x0000011b错误,修复0x0000011b终极指南

在使用Windows操作系统和网络打印机的过程中&#xff0c;很多用户可能遇到了一个常见的错误代码“0x0000011b”。这个问题通常发生在尝试连接或使用网络打印机时&#xff0c;尤其是在安装了特定Windows安全更新后。本文将详细介绍如何快速一键修复此问题&#xff0c;确保您的打…

利用MMDetection将单阶段检测器作为Faster R-CNN的RPN

将单阶段检测器作为RPN 一、在 Faster R-CNN 中使用 FCOSHead 作为 RPNHead与原始配置的对比结果Neck (FPN)RPN HeadROI Head学习率 使用单阶段检测器作为RPN的优势1. 速度提升2. 准确性3. 简化架构4. 灵活性 二、评估候选区域三、用预先训练的 FCOS 训练定制的 Faster R-CNN 本…

开源模型应用落地-FastAPI-助力模型交互-WebSocket篇(五)

一、前言 使用 FastAPI 可以帮助我们更简单高效地部署 AI 交互业务。FastAPI 提供了快速构建 API 的能力,开发者可以轻松地定义模型需要的输入和输出格式,并编写好相应的业务逻辑。 FastAPI 的异步高性能架构,可以有效支持大量并发的预测请求,为用户提供流畅的交互体验。此外,F…

shellhub 部署

1、环境介绍 操作系统&#xff1a;龙蜥os 7.9 2、安装docker yum install -y yum-utils device-mapper-persistent-data lvm2 yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sed -i sdownload.docker.commirrors.aliyun.c…

江协科技51单片机学习- p23 DS1302实时时钟

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

【TB作品】智能台灯,ATMEGA16单片机,Proteus仿真

智能台灯 1 adc检测光强光敏电阻 显示电压 2 光强太高 也就是高于临界值 就关闭小灯 3 光强太低 也就是低于临界值 就打开小灯 3 按键修改临界值 显示 实验报告&#xff1a;基于ATMEGA16单片机的智能台灯设计与Proteus仿真 1. 实验背景 智能台灯是一种能够根据环境光强自动调…

计算机网络-第5章运输层

5.1运输层协议概述 5.1.1进程之间的通信 运输层向它上面的应用层提供通信服务&#xff0c;它属于面向通信部分的最高层&#xff0c;同时也是用户功能中的最低层。 通信的两端应当是两个主机中的应用进程。 运输层复用和分用&#xff1a;复用指在发送方不同的应用进程都可以…

Vue2组件传值(通信)的方式

目录 1.父传后代 ( 后代拿到了父的数据 )1. 父组件引入子组件&#xff0c;绑定数据2. 子组件直接使用父组件的数据3. 依赖注入(使用 provide/inject API)1.在祖先组件中使用 provide2.在后代组件中使用 inject 2.后代传父 &#xff08;父拿到了后代的数据&#xff09;1. 子组件…

【Qt】认识Qt界面Hello world小程序

一.认识Qt界面 1.左边栏 在编辑模式下&#xff0c;左边竖排的两个窗⼝叫做 "边栏" 。 ① 是项⽬⽂件管理窗⼝ ② 是打开⽂件列表窗⼝。 边栏⾥的窗⼝数⽬可以增加&#xff0c;边栏⼦窗⼝标题栏有⼀排⼩按钮&#xff0c;最右边的是关闭按钮&#xff0c;倒数第⼆个是 …

千元好礼等你来拿 MatrixOne最强体验官

开发者集合&#xff01;[MatrixOne最强体验官]带着丰厚的奖品走来啦&#xff01;MatrixOne 是一款高度兼容 MySQL 语法的 HTAP 数据库&#xff0c;MatrixOne Cloud (MO Cloud) 是基于 MatrixOne 内核的全托管云原生数据平台&#xff0c;具备实时 HTAP&#xff0c;多租户&#x…

Unity Shader 软粒子

Unity Shader 软粒子 前言项目Shader连连看项目渲染管线设置 鸣谢 前言 当场景有点单调的时候&#xff0c;就需要一些粒子点缀&#xff0c;此时软粒子就可以发挥作用了。 使用软粒子与未使用软粒子对比图 项目 Shader连连看 这里插播一点&#xff0c;可以用Vertex Color与…

antd(5.x) Popover 的content有个modal,关不掉了

问题描述&#xff1a; 如上图所示&#xff0c;我的提示modal 关不掉了&#xff0c;思考问题症结在handleVisibleChange const content (<div className{styles.box}>别的样式</div>{/* 链接 */}<div className{styles.linkBox}><Modaltitle{提示}open{…

deepin基于apt-mirror同步软件源及构建本地内网源

1.安装apt-mirror sudo apt install -y apt-mirror2.配置apt-mirror(/etc/apt/mirror.list) sudo cp /etc/apt/mirror.list /etc/apt/mirror.list.deepin.bak #备份配置文件 sudo gedit /etc/apt/mirror.list修改如下&#xff1a; deb [trustedyes] https://mirrors.bfsu.ed…

在线如何快速把图片变小?图片轻松修改大小的3个在线工具

随着现在图片在工作和生活中的广泛使用&#xff0c;在使用图片的时候经常会因为图片太大的问题受到影响&#xff0c;比较简单的一种处理方法可以通过压缩图片的方式来缩小图片大小&#xff0c;那么图片压缩具体该怎么来操作呢&#xff1f;下面就给大家分享几款图片在线压缩工具…

python如何求不定积分

sympy介绍 sympy库的安装非常的简单&#xff0c;利用conda命令可以快速的完成安装。 conda install sympy 接下来&#xff0c;我们将介绍利用第三方库sympy来完成积分的计算。 python求解不定积分 接下来&#xff0c;我们将介绍上述的不定积分的求解。 首先导入sympy库中的…

切片的基础知识

文章目录 ● Slice 的底层实现原理&#xff1f;● array 和 Slice 的区别&#xff1f;● 拷贝大切片一定比小切片代价大吗&#xff1f;● Slice 深拷贝和浅拷贝&#xff1f;● 零切片、空切片、nil切片&#xff1f;● Slice 的扩容机制&#xff1f;● Slice 为什么不是线程安全…

父子节点内容和个数提取

有时我们需要获得菜单的内容和个数&#xff0c;这个时候通常有父子菜单&#xff0c;那么怎么分别获取到他们呢&#xff1f;以下面的智慧物业管理系统为例&#xff0c;有7个父节点&#xff0c;每个父节点下面有子节点。如何把父节点名称和总数&#xff0c;以及子节点的名称和总数…

Golang-context理解

golang-context笔记整理 golang为何设计context&#xff1f;代码上理解原理空context类cancelCtx类.withcancelctx方法 timerCtx类valueCtx类 golang为何设计context&#xff1f; 有并发特性的语言中&#xff0c;都会有一种说法&#xff1a;创建异步线程或者携程的时候&#x…

在postman中调试supabase的API接口

文章目录 在supabase中获取API地址和key知道它的restfull风格在postman中进行的设置1、get请求调试2、post新增用户调试3、使用patch更新数据&#xff0c;不用put&#xff01;4、delete删除数据 总结 在supabase中获取API地址和key 首先登录dashboard后台&#xff0c;首页- 右…