数据结构与算法|算法总结|滑动窗口篇

之前在用golang二刷代码随想录的时候,遇到209.长度最小的子数组竟然没想到应该用滑动窗口解题!怒而猛刷,并结合各个博客和视频总结滑动窗口题型和模板如下
参考资料:
【精心总结滑动窗口代码模板, 直接搞定80道Leetcode算法题】
【两分钟搞懂滑动窗口算法,致敬3b1b】
【labuladong算法笔记】

再次强调,主要是体会滑动窗口的核心思想,模板只是辅助。

文章目录

  • 滑动窗口的使用场景
  • 滑动窗口的核心思想
    • 使用思路(寻找最长)
    • 使用思路(寻找最短)
    • 模板
    • 典型题型推荐
      • 209. 长度最小的子数组
      • 3. 无重复字符的最长子串
        • set
        • map
        • 使用数组优化哈希表
      • 438. 找到字符串中所有字母异位词
        • 数组优化
      • 76. 最小覆盖子串

滑动窗口的使用场景

  • 很多题目都是找到所谓的子串、子数组、序列等等
  • 有一些要求:最长、最小、重复等等
  • 条件上的要求满足:串联、覆盖、有无重复、结算结果、出现次数、同时包含XX

一旦出现以上关键词,我们就应该考虑是否能考虑滑动窗口进行解答。

总而言之我认为,它往往是求一个连续的子序列,然后这个子序列要满足某种条件,这种题滑动窗口肯定是可以做的

滑动窗口的核心思想

滑动窗口一般有几个核心组件:

  • 左右指针构成一个窗口
    一般是首先移动右指针,然后判断当前窗内是否满足要求,满足要求存储结果,如果不满足要求了,就开始移动左指针,以求重新满足要求,当左右指针重合,可以认为是满足要求的特殊情况,重新开始移动右指针
  • 需要一个容器来存储结果
    一般是直接定义一个int整型数,在活动窗口中如果满足要求,将结果存储到容器中。

当右指针都到达末尾的时候,即整个流程结束

使用思路(寻找最长)

核心:左右双指针(L, R)在起始点,R向右逐位滑动
每次滑动过程中:
if : 窗口内元素满足条件,R向右扩大窗口,并更新最优结果
if : 窗口内元素不满足条件,L向右缩小窗口
流程结束:R指针到达结尾,整个流程结束

使用思路(寻找最短)

核心:左右双指针(L, R)在起始点,R向右逐位滑动
每次滑动过程中:
if : 窗口内元素满足条件,L向右缩小窗口,并更新最优结果
if : 窗口内元素不满足条件,R向右扩大窗口
流程结束:R指针到达结尾,整个流程结束

模板

两种情况在代码实现中最大的区别就是:内循环中,一个是result不满足要求;一个是当result满足要求!
最长(大)模板

初始化left, right, result, bestResult
某些时候可能需要合适的容器来承载result和bestResult
个人觉得最常用的就是哈希(包括数组、map、set)
while (右指针没有到结尾) {
	窗口扩大,加入right对应元素,更新当前result
	while/if (result不满足要求) {
		窗口缩小,移除left对应元素,left右移
	}
	更新最优结果bestResult
	right++
}
返回bestResult;

这里while/if的区别主要在于如何移动指针,是逐步缩小窗口,还是直接开始新的窗口更新。
最短(小)模板

初始化left, right, result, bestResult
某些时候可能需要合适的容器来承载result和bestResult
个人觉得最常用的就是哈希(包括数组、map、set)
while (右指针没有到结尾) {
	窗口被扩大,加入right对应元素,更新当前result
	while/if (result满足要求) {
		窗口缩小,移除left对应元素,left右移
	}
	更新最优结果bestResult
	right++
}
返回bestResult;

典型题型推荐

以下为最经典的题型:来自代码随想录和LeeCode hot100

  • 209. 长度最小的子数组
  • 3. 无重复字符的最长子串
  • 438. 找到字符串中所有字母异位词
  • 76. 最小覆盖子串

209. 长度最小的子数组

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int right = 0, left = 0;
        int curSum = 0;
        int bestResult = INT_MAX;  // 使用INT_MAX来表示无效的大数,方便之后取最小值

        while (right < nums.size()) {
            curSum += nums[right];  // 扩展窗口右边界
            while (curSum >= target) {
                bestResult = min(bestResult, right - left + 1);  // 更新最短长度
                curSum -= nums[left];  // 缩小窗口
                left++;  // 正确的是left增加
            }
            right++;  // 继续向右扩展窗口
        }

        return bestResult == INT_MAX ? 0 : bestResult;  // 如果没有更新过bestResult,返回0
    }
};

3. 无重复字符的最长子串

本题最核心的就是选一个容器来装字符:

set
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_set<char> set;  // 用于存储窗口中的字符
        int left = 0, right = 0;  // 双指针,表示当前的滑动窗口[left, right)
        int bestResult = 0;       // 最长无重复字符的子串长度

        while (right < s.length()) {
            char rChar = s[right];  // 右指针对应的字符
            // 如果字符已经存在于set中,移动左指针直到移除重复字符
            while (set.find(rChar) != set.end()) {
                set.erase(s[left]);
                left++;
            }
            // 添加新字符到set中,更新结果,移动右指针
            set.insert(rChar);
            bestResult = max(bestResult, right - left + 1);
            right++;
        }

        return bestResult;
    }
};
map

第一反应是使用unordered_map,我们可以在map中key为出现的字符,value为该字符的下标,方便左边的更新,下边的数组优化方案也是一样的道理,所以并这类题并不需要循环

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_map<char, int> map;  // 字符到其最近出现位置的映射
        int left = 0, right = 0;       // 双指针,表示当前的滑动窗口[left, right)
        int result = 0;                // 当前窗口的长度
        int bestResult = 0;            // 最长无重复字符的子串长度

        while (right < s.length()) {
            char rChar = s[right];     // 右指针对应的字符
            if (map.find(rChar) != map.end()) {
                // 如果字符已经存在,则可能需要移动左指针
                left = max(left, map[rChar] + 1);
            }
            map[rChar] = right;        // 更新或添加字符的最新索引
            result = right - left + 1; // 更新当前窗口的长度
            bestResult = max(bestResult, result); // 更新最长子串的长度
            right++;                   // 移动右指针
        }

        return bestResult;
    }
};
使用数组优化哈希表
int lengthOfLongestSubstring(string s) {
    vector<int> index(256, -1);  // ASCII 字符集,所有元素初始化为 -1
    int left = 0, right = 0;
    int bestResult = 0;

    while (right < s.length()) {
        char rChar = s[right];
        // 如果当前字符已出现过且索引大于等于左指针,则更新左指针
        if (index[rChar] != -1 && index[rChar] >= left) 
            left = index[rChar] + 1;
        // 更新当前字符的索引
        index[rChar] = right;
        // 计算当前无重复字符子串的长度,并更新最长长度
        bestResult = max(bestResult, right - left + 1);
        // 移动右指针
        right++;
    }

    return bestResult;
}

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

拿到本题很直观的感觉,我们需要一个容器装p,记录他出现的字母和次数,然后需要一个容器装我们滑动窗口中的字符,然后这两个容器如果匹配上了,那肯定就是异构词了。

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> result;
        if (s.size() < p.size()) return result;

        unordered_map<char, int> pCount, sCount;

        //初始化p的字符频率表
        for (char c : p) {
            pCount[c]++;
        }

        int left = 0, right = 0;
        int required = p.size();
        while (right < s.size()) {
            //加入当前右指针
            char rChar = s[right];
            sCount[rChar]++;

            //当窗口大小匹配P长度时进行比较
            if (right - left + 1 == required) {
                if (sCount == pCount) { //存结果
                    result.push_back(left);
                }
                //否则窗口左端向右移动,缩小窗口
                sCount[s[left]]--;
                if (sCount[s[left]] == 0) {
                    sCount.erase(s[left]);
                }
                left++;
            }
            right++;
        }
        return result;
    }
};
数组优化
  • 我们使用固定大小的数组代替哈希表
  • 减少不必要的比较
    • 我们在之前的实现,每次窗口大小达到p长度时,我们都会比较两个哈希表,我们可以只在字符频率匹配时才进行这样的比较,即当插入或删除操作可能改变频率表到匹配状态时才检查
  • 滑动窗口计数
    • 维护一个计数器来跟踪已匹配的字符种类数量。例如,当某个字符的期望频率与窗口中的频率相匹配时,增加计数器。如果所有字符都匹配,计数器将等于不同字符的总数。这样可以在不比较整个哈希表(指数组)的情况下,通过检查计数器来判断当前窗口是否为有效的异位词。
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> result;
        if (s.size() < p.size()) return result;

        vector<int> pCount(256, 0), sCount(256, 0);
        for (char c : p) {
            pCount[c]++;
        }

        int left = 0, right = 0, count = 0;
        int pLength = p.size();

        while (right < s.size()) {
            // 增加右指针字符
            if (pCount[s[right]] > 0 && ++sCount[s[right]] <= pCount[s[right]]) {
                count++;
            }
            // 当窗口大小正确,并且计数匹配
            if (right - left + 1 == pLength) {
                if (count == pLength) {
                    result.push_back(left);
                }
                // 减少左指针字符
                if (pCount[s[left]] > 0 && sCount[s[left]]-- <= pCount[s[left]]) {
                    count--;
                }
                left++;
            }
            right++;
        }
        return result;
    }
};

76. 最小覆盖子串

和上一题几乎一样

class Solution {
public:
    string minWindow(string s, string t) {
        if (s.empty() || t.empty() || s.size() < t.size()) return "";

        // 字符计数数组
        unordered_map<char, int> need, have;
        for (char c : t) {
            need[c]++;
        }

        // required 表示需要涵盖的字符种类数
        int required = need.size();
        int left = 0, right = 0, formed = 0;
        int minLen = INT_MAX, minStart = 0; // 用于记录最小子串的起始位置和长度

        while (right < s.length()) {
            char c = s[right];
            have[c]++;

            // 如果当前字符的数量符合需求的数量,增加formed
            if (need.count(c) && have[c] == need[c]) {
                formed++;
            }

            // 尝试缩小窗口,直到窗口不再满足条件
            while (left <= right && formed == required) {
                char temp = s[left];
                // 更新最小窗口
                if (right - left + 1 < minLen) {
                    minLen = right - left + 1;
                    minStart = left;
                }
                // 移动左指针,更新have数组和formed计数
                have[temp]--;
                if (need.count(temp) && have[temp] < need[temp]) {
                    formed--;
                }
                left++;
            }

            // 移动右指针
            right++;
        }

        return minLen == INT_MAX ? "" : s.substr(minStart, minLen);
    }
};

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

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

相关文章

hypertherm海宝EDGE控制器显示屏工控机维修

海宝工控机维修V3.0/4.0/5.0&#xff1b;hypertherm数控切割机系统MICRO EDGE系统显示屏维修&#xff1b; 美国hypertherm公司mirco edge数控系统技术标准如下&#xff1a; 1&#xff09; p4处理器 2&#xff09; 512mb内存 3&#xff09; 80g硬盘&#xff0c;1.44m内置软驱…

AXI Block RAM 控制器IP核的用法详解

本文描述了如何使用Xilinx的Vivado Design Suite环境中的工具来定制和生成AXI Block RAM (BRAM) IP 核。Vivado Design Suite是一个强大的FPGA设计和开发环境&#xff0c;它允许用户定制和配置各种IP核以适应他们的特定设计需求。 以下是针对如何定制IP核的步骤的简要概述&…

【FX110】2024外汇市场中交易量最大的货币对是哪个?

作为最大、最流动的金融市场之一&#xff0c;外汇市场每天的交易量高达几万亿美元&#xff0c;涉及到数百种货币。不同货币对的交易活跃程度并不一样&#xff0c;交易者需要根据货币对各自的特点去进行交易。 全年外汇市场中涉及美元的外汇交易超过50%&#xff01; 实际上&…

docker学习笔记(四)制作镜像

目录 第1步&#xff1a;编辑Dockerfile 第2步&#xff1a;编辑requirements.txt文件 第3步&#xff1a;编辑app.py文件&#xff0c;我们的程序文件 第4步&#xff1a;生成镜像文件 第5步&#xff1a;使用镜像&#xff0c;启动容器 第6步&#xff1a; 启动redis容器、将容器…

开启智慧生活,家政服务触手可及——家政小程序全新上线

繁忙生活中的贴心助手 在快节奏的现代生活中&#xff0c;我们时常为家庭琐事所困扰&#xff0c;无暇享受生活的美好。为了帮助您解决这一难题&#xff0c;我们倾力打造了一款家政小程序&#xff0c;让您的生活更加轻松、便捷。 家政小程序&#xff0c;您的生活管家 1. 全方位…

社媒营销中的截流获客是怎么一回事?

如果你要问&#xff0c;现在做社媒营销是通过哪些方式进行引流的&#xff0c;那么必然有一种是截流&#xff0c;顾名思义也就是分取别人的流量&#xff0c;方法其实很简单&#xff0c;主要分为两种&#xff1a;&#xff08;1&#xff09;抓取别人的粉丝出来进行群发私信&#x…

nestjs 全栈进阶--Module和Provider的循环依赖

视频教程 21_nest中的循环依赖_哔哩哔哩_bilibili 1. 循环依赖 当两个类相互依赖时&#xff0c;就会发生循环依赖。比如 A 类需要 B 类&#xff0c;B 类也需要 A 类。Nest中 模块之间和 提供器之间也可能会出现循环依赖。 nest new dependency -p pnpm nest g res aaa --n…

【Java EE】网络原理——UDP

目录 1.应用层 2.传输层 2.1端口号 2.1.1端口号的范围划分 2.1.2一个端口号可以被多个进程绑定吗&#xff1f; 2.1.3一个进程可以绑定多个端口号吗&#xff1f; 3.UDP协议 3.1UDP的格式 3.1.1 UDP的源端口号 3.1.2 UDP的目的端口号 3.1.3 UDP长度 3.1.4UDP校验和 3…

springboot项目中前端页面无法加载怎么办

在springboot前后端分离的项目中&#xff0c;经常会出现前端页面无法加载的情况&#xff08;比如&#xff1a;前端页面为空白页&#xff0c;或者出现404&#xff09;&#xff0c;该怎么办&#xff1f;&#xff1f;&#xff1f; 一个简单有效的方法&#xff1a;&#xff1a; 第…

24 | MySQL是怎么保证主备一致的?

MySQL 主备的基本原理 内部流程 备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。一个事务日志同步的完整过程是这样的: 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始…

钉钉群定时发送消息1.0软件【附源码】

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 有时候需要在钉钉群里提醒一些消息。要通知的群成员又不方便用定时钉的功能&#xff0c;所以写了这么一个每日定时推送群消息的工具。 易语言程序&#xff0c;附上源码与模块&#x…

【记录42】centos 7.6安装nginx教程详细教程

环境&#xff1a;腾讯云centos7.6 需求&#xff1a;安装nginx-1.24.0 1. 切入home文件 cd home 2. 创建nginx文件 mkdir nginx 3. 切入nginx文件 cd nginx 4. 下载nginx安装包 wget https://nginx.org/download/nginx-1.24.0.tar.gz 5. 解压安装包 tar -zxvf nginx-1.24.0.…

ESD静电问题 | 选型TVS单向还是双向?

【转自微信公众号&#xff1a;Amazing晶炎科技】

Mysql进阶-索引篇

Mysql进阶 存储引擎前言特点对比 索引介绍常见的索引结构索引分类索引语法sql分析索引使用原则索引失效的几种情况sql提示覆盖索引前缀索引索引设计原则 存储引擎 前言 Mysql的体系结构&#xff1a; 连接层 最上层是一些客户端和链接服务&#xff0c;主要完成一些类似于连接…

C语言例题38、有n个人围成一圈,顺序排号。从第一个人开始报数(从1到3报数),凡报到3的人退出圈子,最后留下来的是原来第几号人员?

#include <stdio.h> #define MAX_CALLER 3void main() {int j 0;int p_total;//人数int p_caller 0;//每3人循环计数&#xff1a;1,2,3int p_exit 0; //退出游戏的人数int people[255] {0};//参与游戏人员名单printf("请输入参与游戏人数&#xff1a;");s…

CCF-Csp算法能力认证,202206-1归一化处理(C++)含解析

前言 推荐书目&#xff0c;在这里推荐那一本《算法笔记》&#xff08;胡明&#xff09;&#xff0c;需要PDF的话&#xff0c;链接如下 「链接&#xff1a;https://pan.xunlei.com/s/VNvz4BUFYqnx8kJ4BI4v1ywPA1?pwd6vdq# 提取码&#xff1a;6vdq”复制这段内容后打开手机迅雷…

Macbook pnpm 安装 node-sass 报错(node-gyp)

换了 Macbook M3 Pro 后安装项目依赖时报错&#xff0c;提示 node-sass 安装出错。 &#xff08;此外&#xff0c;ValueError: invalid mode: rU while trying to load binding.gyp 也是类似原因。只需要确保 node-gyp 运行条件就可以&#xff09; 原因是 node-gyp 运行环境缺…

手写SpringBoot核心功能流程

本文通过手写模拟实现一个简易版的Spring Boot 程序&#xff0c;让大家能以非常简单的方式知道Spring Boot大概的工作流程。 工程依赖 创建maven工程&#xff0c;并创建两个module springboot模块&#xff1a;手写模拟springboot框架的源码实现 test模块&#xff1a;业务系统…

提升工作效率,用ONLYOFFICE打造高效团队协作环境

作为一名深耕技术领域已有六七年的开发者&#xff0c;同时又是断断续续进行技术创作将近六年的一个小小作者&#xff0c;我在工作和日常生活中&#xff0c;使用过各色各样的软件。 而在最近几年&#xff0c;一款名为ONLYOFFICE的开源办公套件逐渐走进并融入我的工作与生活&…

使用Vue连接Mqtt实现主题的订阅及消息发布

效果如下&#xff1a; 直接贴代码&#xff0c;本地创建一个html文件将以下内容贴入即可 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, …
最新文章