《滑动窗口算法:从 “暴力遍历” 到 “线性高效” 的思维跃迁》

**前引:**在处理数组、字符串的子串 / 子数组问题时,你是否也曾陷入 "暴力遍历" 的泥潭?比如找最长无重复子串、最小覆盖子串,或是区间和满足条件的最短长度 ------ 暴力解法往往需要嵌套循环,时间复杂度飙升至 O (n²),面对大数据量时直接 "超时"。而滑动窗口算法,正是为解决这类 "区间查询" 问题而生的 "高效工具":它通过两个指针模拟一个 "可伸缩的窗口",在一次线性遍历中完成区间筛选,将时间复杂度直接优化到 O (n)。本文将从核心原理出发,拆解滑动窗口的 "窗口收缩 / 扩张" 逻辑,带你掌握固定窗口、可变窗口的通用模板,从此告别嵌套循环的低效困境!

目录

"滑动窗口"算法介绍

【一】长度最小的子树组

(1)链接

(2)算法解析

(3)代码

(4)常用接口:max/min

【二】无重复长度最长子串

(1)链接

(2)算法解析

(3)代码

(4)重要接口:

【三】最小覆盖子串(重要)

(1)链接

(2)算法解析

(3)优化(核心)

(4)代码


"滑动窗口"算法介绍

废话不多说,直接介绍:

"滑动窗口"也是依赖两个指针的移动,如果两个指针同向移动,那么可称为滑动窗口

比如一个毛毛虫,向一方移动,身体长度在移动的情况下不断变化!

【一】长度最小的子树组

(1)链接

https://leetcode.cn/problems/minimum-size-subarray-sum

(2)算法解析

要求:通过找一段区间,满足区间内的数字之和 >= target,求最短区间

暴力解法:强力枚举,从第一个数字 left 开始,让 right 不断向右,直达和满足要求

再从第二个数字开始,right回到left位置,重新求和.....不断循环

算法:

假设一段数组:【2,3,1,2,4,3】,定义left、right=0;target=7;

此时第一段有效区间是【2,3,1,2】,即left指向位置为2(0),right指向位置位2(3)

从第二次查找开始,暴力解法是让left和right都回到3(1)的位置,但是满足如下规律:

即left和right直接有一部分元素是重复的,right不需要回到left位置,如果此时【left,right】范围内的和满足要求继续右移left,否则right右移,right移动到数组末尾,更新为最后一次结果即完成

(3)代码
cpp 复制代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums)
    {
        if(nums.size()==0)return 0;
        int left = 0;
        int right = left;
        int len = INT_MAX;
        int sum = 0;
        while (right < nums.size())
        {
            sum += nums[right];
            //进窗口,如果一直进不了循环呢
            while (sum >= target)
            {
                if (sum >= target)
                {
                    if (right - left < len)
                    {
                        len = right - left + 1;
                    }
                }
                sum-=nums[left++];
            }
            right++;
        }
        
        if(len==INT_MAX)return 0;
        return len;
    }
};
(4)常用接口:max/min

求最大值或者最小值,可以直接使用接口:max/min(x1,x2),返回x1和x2对应的最值

通常整型最大值为INT_MAX,最小值在算法题中通常设置为-1

【二】无重复长度最长子串

(1)链接

https://leetcode.cn/problems/longest-substring-without-repeating-characters

(2)算法解析

暴力枚举:left与right从0下标开始,left固定,right不断向后移动,碰到范围重复的就停下来

left++,right回到left位置,继续依次循环,如下图:

算法解析:我们看图,找到重复之后,left++,right就没必要回来了,反正right一定移动到上一次结尾的下一个位置,所以遇到重复的就left++,否则right一直向前。范围里的元素出现次数都为1

(3)代码
cpp 复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) 
    {
        //如果是空串返回0
        if(s.empty())return 0;
        //建立哈希表
        unordered_set<char> V;
        //双指针
        int left=0;
        int right=0;
        int len=0;
        while(right<s.size())
        {
            //只要窗口包含当前right的字符,就收缩left
            while (V.count(s[right])) 
            { 
                V.erase(s[left++]);
            }
            //插入当前字
            V.insert(s[right++]);
            if (right - left > len) 
            {
                len = right - left;
            }
        }
        return len;
    }
};
(4)重要接口:

有两个容器是滑动窗口用的最多的:unordered_set<T> unordered_map<T,T>

其中**【】**根据key(没有就创建)返回value是 map 容器常用的接口

其中有一个接口可以判断是否重复,在本题很实用:count(key),key重复就返回非0

【三】最小覆盖子串(重要)

(1)链接

https://leetcode.cn/problems/minimum-window-substring

(2)算法解析

暴力枚举:left、right开始为0,先让right++,一直找到该范围包含全部的子串,主要是如何找?

借助unordered_map<char,int> 和【】(准备俩个该容器对象)最简单的就是 find ,如果hash1能find(s[right])且在 hash2 中找到对应字符且对应 value 相等,说明单个元素是符合条件

(1)单个元素符合条件:借助map和【】,确定单个元素是否一致

(2)比如 t = AA,有效元素个数为1,t = BA,有效元素个数为2,即不重复的字符数量相等

满足上面两个条件之后,那么范围就被确定下来了,此时left右移之后,重新划分范围

算法解析:算法解析也就是不让right每次回来,即一直向右,这没什么好说的,因为有重复的元素

(3)优化(核心)

这题的主要困难就是 t 中的元素可能重复,否则直接被 **unordered_map<char,int>**秒杀,因此要解决重复的情况,可以引入一个变量 count ,先用 hash2 【】遍历 t 中的元素,获得有效元素个数,当 right 每次找到一个元素就让 hash1 【】该元素,如果该元素【】的value和hash2的相等,就count++,当count和"获得有效元素个数"相等,即单次范围划分完毕!count是种类不是个数

(4)代码
cpp 复制代码
class Solution 
{
public:
 string minWindow(string s, string t) 
 {
    int hash1[128] = { 0 }; // 统计字符串 t 中每⼀个字符的频次 
    int kinds = 0; // 统计有效字符有多少种 
    for(auto ch : t)
    if(hash1[ch]++ == 0) kinds++;
    int hash2[128] = { 0 }; // 统计窗⼝内每个字符的频次 
    int minlen = INT_MAX, begin = -1;
    
    for(int left = 0, right = 0, count = 0; right < s.size(); right++)
    {
        char in = s[right];
        if(++hash2[in] == hash1[in]) count++; // 进窗⼝ + 维护 count 
        while(count == kinds) // 判断条件 
        {
            if(right - left + 1 < minlen) // 更新结果 
            {
            minlen = right - left + 1;
            begin = left;
            }
            char out = s[left++];
            if(hash2[out]-- == hash1[out]) count--; // 出窗⼝ + 维护 count 
        }
    }
    if(begin == -1) return "";
    else return s.substr(begin, minlen);
    }
};
相关推荐
惊讶的猫14 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
文艺理科生Owen15 小时前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
运维·nginx
不爱缺氧i15 小时前
完全卸载MariaDB
数据库·mariadb
期待のcode15 小时前
Redis的主从复制与集群
运维·服务器·redis
翼龙云_cloud15 小时前
腾讯云代理商: Linux 云服务器搭建 FTP 服务指南
linux·服务器·腾讯云
纤纡.15 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql
jiunian_cn15 小时前
【Redis】渐进式遍历
数据库·redis·缓存
REDcker15 小时前
gRPC开发者快速入门
服务器·c++·后端·grpc
橙露15 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot
晚霞的不甘15 小时前
Flutter for OpenHarmony 可视化教学:A* 寻路算法的交互式演示
人工智能·算法·flutter·架构·开源·音视频