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

**前引:**在处理数组、字符串的子串 / 子数组问题时,你是否也曾陷入 "暴力遍历" 的泥潭?比如找最长无重复子串、最小覆盖子串,或是区间和满足条件的最短长度 ------ 暴力解法往往需要嵌套循环,时间复杂度飙升至 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);
    }
};
相关推荐
钮钴禄·爱因斯晨15 小时前
聚焦操作系统中的PV操作
数据库·算法·系统架构·c#
2301_8135995515 小时前
CSS中relative与absolute的区别_详解相对与绝对定位应用场景
jvm·数据库·python
云泽80815 小时前
笔试算法 - 双指针篇(一):移动零、复写零、快乐数与盛水容器
c++·算法
qq_3721542315 小时前
c++怎么在写入文件流时通过peek预读功能实现复杂的逻辑判断【实战】
jvm·数据库·python
Dragon水魅15 小时前
爬虫技术详解:从传统爬虫到浏览器自动化——以豆瓣读书笔记为例
运维·爬虫·自动化
m0_5145205715 小时前
CSS如何给按钮添加按下缩小的动画_利用-active配合transform
jvm·数据库·python
willhuo15 小时前
# 自动化数据采集技术研究与实现:基于Playwright的抖音网页自动化方案
运维·selenium·c#·自动化·chrome devtools·webview
yejqvow1215 小时前
CSS如何制作加载时的点点点跳动效果_使用animation循环延迟
jvm·数据库·python
2401_8359568115 小时前
CSS如何解决CSS引入后的样式覆盖_理解优先级原则避免重写
jvm·数据库·python
IP老炮不瞎唠15 小时前
IP轮换机制解析:动态住宅代理如何维持高可用率?
运维·服务器·网络