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

news2024/12/28 9:33:57

大家好,我是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;
    }
}
  • 所谓的算法优化都是建立在暴力解法的基础之上,正是看到了暴力解法的冗余,才想到优化的算法

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Centos安装1Panel面板工具安装可视化界面

1Panel是一种市场调研平台&#xff0c;旨在帮助企业进行市场研究和获取消费者反馈。它通过在线调查和观察研究的方式&#xff0c;帮助企业了解他们的目标市场&#xff0c;并针对市场需求做出相应的决策。 1Panel的特点包括&#xff1a; 1. 全球范围&#xff1a;1Panel在全球范…

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

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

WPS图片无法居中、居中按钮无法点击(是灰色的)

在PPT中复制对象到WPS word中后&#xff0c;导致图片一直靠左&#xff0c;而无法居中 直接选中图片是错误的&#xff1a; 这时你会发现居中按钮无法点击&#xff08;是灰色的&#xff09; 正确的是选中图片的前面的部分&#xff0c;然后点击居中&#xff0c;或者Ctrl E

免费开源的后端API服务-supabase安装和使用-简直是前端学习者福音

文章目录 它是什么安装和部署关于安装关于部署1、注册用户2、创建组织3、创建项目 创建数据库表&#xff08;填充内容&#xff09;填充数据库表 使用postman联调API 它是什么 一个开源免费的后端框架&#xff0c;firebase的替代品。可以简单理解类似于headless cms&#xff0c…

气膜建筑锚固系统:稳如泰山的保护屏障—轻空间

在建设气膜建筑时&#xff0c;很多人都会担心它是否能在强风或恶劣天气中保持稳定。然而&#xff0c;气膜建筑的锚固系统使得这一担忧完全多余。轻空间将揭秘气膜建筑锚固系统的独特设计和卓越性能&#xff0c;展示其如何保证气膜建筑在任何天气条件下都能稳如泰山。 气膜建筑锚…

Error: A JNl error has occurred, please check your installation and try again.

Eclipse 运行main方法的时候报错&#xff1a;Error: A JNl error has occurred, please check your installation and try again. 一、问题分析 导致这个问题&#xff0c;主要原因&#xff0c;我认为是在新版本中&#xff0c;默认的JDK编译版本与我们配置的JDK版本不一致导致的…

JavaScript将参数传递给事件处理程序

本篇文件我们将实现导航栏中&#xff0c;选中时候&#xff0c;会将您选中的进行高亮显示&#xff1b; ● 首先我们来获取我们想要的HTML元素 const nav document.querySelector(.nav);● 接着我们来写选中的高亮显示 nav.addEventListener(mouseover, function (e) { //鼠…

公网环境使用Potplayer远程访问家中群晖NAS搭建的WebDAV听歌看电影

文章目录 前言1 使用环境要求&#xff1a;2 配置webdav3 测试局域网使用potplayer访问webdav4 内网穿透&#xff0c;映射至公网5 使用固定地址在potplayer访问webdav 前言 本文主要介绍如何在Windows设备使用potplayer播放器远程访问本地局域网的群晖NAS中的影视资源&#xff…

NC13611 树(dfs序+区间dp)

链接 思路&#xff1a; 容易知道对于同一种颜色的子图一定是仅由该颜色的点连通的。设我们要划分的个数为x&#xff08;x<k&#xff09;&#xff0c;也就是说我们要选出x-1条边&#xff0c;这里有种情况。那么我们需要选出x种颜色&#xff0c;这里有种情况。然后我们需要将…

事过无悔:人生中的释怀之道

在纷繁复杂的人生旅途中&#xff0c;我们常常会面临各种选择。这些选择&#xff0c;如同指引我们前行的路标&#xff0c;有时让我们欣喜&#xff0c;有时让我们遗憾。然而&#xff0c;我渐渐发现&#xff0c;事过无悔&#xff0c;是我们在面对这些选择时最顶级的释怀之道。 首…

CNN的小体验

用的pytorch。 训练代码cnn.py&#xff1a; import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms import torch.nn.functional as F# 定义超参数 num_epochs 10 batch_size 100 learning_rat…

2024第17届中国西部(重庆)留学移民海外置业展览会

2024第17届中国西部&#xff08;重庆&#xff09;留学移民海外置业展览会 邀请函 主办单位&#xff1a; 中国西部教体医融合博览会组委会 承办单位&#xff1a;重庆中博展览有限公司 展会背景&#xff1a; 成都和重庆是中国新一线城市&#xff0c;是西部经济的核心增长极&a…

samba服务的搭建与使用

关闭selinux #暂时关闭selinux 查看selinux状态 [rootlocalhost ~]# getenforce Disabled [rootlocalhost ~]# 如果此处是‘enforcing’&#xff0c;则执行下列代码 [rootlocalhost ~]# setenforce 0 再次查看selinux状态 [rootlocalhost ~]# getenforce permissive #永久关…

舞会无领导:一种树形动态规划的视角

没有上司的舞会 Ural 大学有 &#x1d441; 名职员&#xff0c;编号为1∼&#x1d441;。 他们的关系就像一棵以校长为根的树&#xff0c;父节点就是子节点的直接上司。 每个职员有一个快乐指数&#xff0c;用整数 &#x1d43b;&#x1d456; 给出&#xff0c;其中1≤&…

【Llama 2的使用方法】

Llama 2是Meta AI&#xff08;Facebook的母公司Meta的AI部门&#xff09;开发并开源的大型语言模型系列之一。Llama 2是在其前身Llama模型的基础上进行改进和扩展的&#xff0c;旨在提供更强大的自然语言处理能力和更广泛的应用场景。 以下是Llama 2的一些关键特性和更新点&am…

1Python的Pandas:基本简介

1. Pandas的简介 Pandas 是一个开源的 Python 数据分析库&#xff0c;由 Wes McKinney 在 2008 年开始开发&#xff0c;目的是为了解决数据分析任务中的各种需求。Pandas 是基于 NumPy 库构建的&#xff0c;它使得数据处理和分析工作变得更加快速和简单。Pandas 提供了易于使用…

mac|浏览器链接不上服务器但可以登微信

千万千万千万不要没有关梯子直接关机&#xff0c;不然就会这样子呜呜呜 设置-网络&#xff0c;点击三个点--选择--位置--编辑位置&#xff08;默认是自动&#xff09; 新增一个&#xff0c;然后选中点击完成 这样就可以正常上网了

网络编程:UDP编程笔记

1.字节序的概念和转换 小端格式: 低位字节数据存储在低地址 大端格式: 高位字节数据存储在低地址 在主机上时为小端存储,在网络上时为大端,所以接收到数据时,要转为小端口 如下图: #include <arpa/inet.h> 发送者调用的函数: uint32_t htonl(uint32_t hostlong); //转…

【工具推荐】ONLYOFFICE8.1版本编辑器测评——时下的办公利器

文章目录 一、产品介绍1. ONLYOFFICE 8.1简介2. 多元化多功能的编辑器 二、产品体验1. 云端协作空间2. 桌面编辑器本地版 三、产品界面设计1. 本地版本2. 云端版本 四、产品文档处理1. 文本文档&#xff08;Word)2. 电子表格&#xff08;Excel&#xff09;3. PDF表单&#xff0…

Linux——移动文件或目录,查找文件,which命令

移动文件或目录 作用 - mv命令用于剪切或重命名文件 格式 bash mv [选项] 源文件名称 目标文件名称 注意 - 剪切操作不同于复制操作&#xff0c;因为它会把源文件删除掉&#xff0c;只保留剪切后的文件。 - 如果在同一个目录中将某个文件剪切后还粘贴到当前目录下&#xff0c;…