当前位置: 首页 > news >正文

滑动窗口:解决连续区间问题的黄金模板(练习笔记)

滑动窗口:解决连续区间问题的黄金模板!

由于此问题分析的对象是「⼀段连续的区间」,因此可以考虑「滑动窗⼝」的思想来解决这道题。

滑动窗口其实就是双指针的升级版,但此时这两个指针的移动方向是一致的

长度最小的子数组

题⽬链接:

长度最小的子数组

要点:

  • 寻找连续子数组使其和 ≥ target
  • 滑动窗口维护当前窗口和,动态调整左右边界

老师代码:

class Solution
{
public:
    int minSubArrayLen(int target, vector<int>& nums)
    {
        int n = nums.size(), sum = 0, len = INT_MAX;
        for(int left = 0, right = 0; right < n; right++)
        {
            sum += nums[right]; // 进窗⼝
            while(sum >= target) // 判断
            {
                len = min(len, right - left + 1); // 更新结果
                sum -= nums[left++]; // 出窗⼝
            }
        }
        return len == INT_MAX ? 0 : len;
    }
};

老师思路:

  1. 由于此问题分析的对象是**「⼀段连续的区间」,因此可以考虑「滑动窗⼝」**的思想来解决这道题。

    让滑动窗⼝满⾜:从 i 位置开始,窗⼝内所有元素的和⼩于 target (那么当窗⼝内元素之和第⼀次⼤于等于⽬标值的时候,就是 i 位置开始,满⾜条件的最⼩⻓度)。 做法:将右端元素划⼊窗⼝中,统计出此时窗⼝内元素的和

  2. 如果窗⼝内元素之和⼤于等于 target :更新结果,并且将左端元素划出去的同时继续判断是否满⾜条件并更新结果(因为左端元素可能很⼩,划出去之后依旧满⾜条件),如果窗⼝内元素之和不满⾜条件: right++ ,另下⼀个元素进⼊窗⼝

我的代码:

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int ret = 0, sum = 0;
        int left = 0, right = 0;

        //第一次情况
        sum += nums[right];
        while(sum < target)
        {
            right++;
            if(right >= nums.size()) return 0;//边界问题
            sum += nums[right];
        }
        ret = right - left + 1;
        
        while(right < nums.size())
        {
            while(sum >= target)
            {
                ret = right - left + 1 < ret ? right - left + 1 : ret;
                sum -= nums[left++];
            }
            right++;
            if(right < nums.size()) sum += nums[right];
            
        }
        return ret;
    }
};

我的思路:

  1. 初始化左右指针left=0, right=0,窗口和sum=0
  2. right右移扩大窗口,累加元素值到sum
  3. sum ≥ target时,记录窗口长度,并尝试左移left缩小窗口
  4. 最终返回最小窗口长度

我的笔记:

  1. len(ret)最开始时不能定义成0,要不然没有比0更小的数组了,老师的想法就是len = INT_MAX,我的想法就是先找到第一个满足条件的子数组,把这个数组的长度作为ret的初始值

  2. 这个数组的所有元素都是正整数,因此right++,sum += nums[right] 的值一定是增加的,left–,sum -= nums[left]的值一定是减小的,因此可以使用滑动窗口来解决

  3. 下标每一次变化,如果要使用下标对应的元素,都要判断一下边界问题

  4. 先找到第一个满足条件的窗口,作为初始结果

  5. 所有元素为正数,保证窗口和单调性,适合滑动窗口

  6. 边界处理:当数组总和小于target时返回0

无重复字符的最长子串

题⽬链接:

无重复字符的最长子串

要点:

  • 寻找不含重复字符的最长连续子串
  • 哈希表记录字符最后一次出现的位置

老师代码:

class Solution
{
    public:
    int lengthOfLongestSubstring(string s)
    {
        int hash[128] = { 0 }; // 使⽤数组来模拟哈希表
        int left = 0, right = 0, n = s.size();
        int ret = 0;
            while(right < n)
            {
                hash[s[right]]++; // 进⼊窗⼝
                while(hash[s[right]] > 1) // 判断
                    hash[s[left++]]--; // 出窗⼝
                ret = max(ret, right - left + 1); // 更新结果
                right++; // 让下⼀个元素进⼊窗⼝
            }
        return ret;
    }
};

老师思路:

  • 研究的对象依旧是⼀段连续的区间,因此继续使⽤「滑动窗⼝」思想来优化。

  • 让滑动窗⼝满⾜:窗⼝内所有元素都是不重复的。

  • 做法:右端元素 ch 进⼊窗⼝的时候,哈希表统计这个字符的频次:

    ▪ 如果这个字符出现的频次超过 1 ,说明窗⼝内有重复元素,那么就从左侧开始划出窗⼝,直到 ch 这个元素的频次变为 1 ,然后再更新结果。

    ▪ 如果没有超过 1 ,说明当前窗⼝没有重复元素,可以直接更新结果

image-20250222125530474

我的代码:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int arr[128] = {0};//相当于状态寄存器
        int left = 0, right = 0, ret = 0;
        while(right < s.size() )
        {
            if(arr[(int)s[right]] == 0)//如果这个字符没有出现过
            {
                arr[(int)s[right]]++;//对应标志位置1
                right++;//进窗口
            }
            else//如果出现过,开始出窗口
            {
                while(left < right && s[left] != s[right])//循环删除
                {
                    arr[(int)s[left]] = 0;//标志位重新置0
                    left++;//出窗口
                }
                //到了相等的位置,出窗口
                arr[(int)s[left]] = 0;//标志位重新置0
                left++;//出窗口
            }
            //更新结果
            ret = max(ret, right - left);
        }
        return ret;
    }
    
};

我的思路:

  1. 使用数组hash[128]模拟哈希表,记录字符是否在窗口内
  2. right右移时检查字符是否重复
  3. 若重复,移动left到重复字符的下一个位置,并更新哈希表

我的笔记:

  1. 哈希表直接存储字符的存在状态,时间复杂度O(n)
  2. 左指针跳跃式移动,避免无效检查
  3. 关键代码:ret = max(ret, right - left)
最大连续1的个数

题⽬链接:

最大连续1的个数

要点:

  • 允许翻转k个0为1,求最长连续1子数组
  • 将问题转化为“窗口内0的个数不超过k”

老师代码:

class Solution
{
public:
    int longestOnes(vector<int>& nums, int k)
    {
        int ret = 0;
        for(int left = 0, right = 0, zero = 0; right < nums.size(); right++)
        {
            if(nums[right] == 0) zero++; // 进窗⼝
            while(zero > k) // 判断
                if(nums[left++] == 0) zero--; // 出窗⼝
            ret = max(ret, right - left + 1); // 更新结果
        }
        return ret;
    }
}

老师思路:

不要去想怎么翻转,不要把问题想的很复杂,这道题的结果⽆⾮就是⼀段连续的 1 中间塞了 k 个 0 嘛。 因此,我们可以把问题转化成:求数组中⼀段最⻓的连续区间,要求这段区间内 0 的个数不超过 k 个。

我的代码:

class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int num = 0, left = 0, right = 0;//right是下一个要判断的数字
        int ret = 0;
        while(right < nums.size())
        {
            if(nums[right] == 0)//如果为0,判断是否还有翻转名额
            {
                if(num >= k)//没有翻转名额
                {
                    //出窗口
                    while(left < right && nums[left] != 0) left++;//left找到最近的0位置
                    left++;//left来到最近的0位置的后一个位置
                    num--;//释放一个名额
                }
                else//名额还有
                {
                    //进窗口
                    num++;
                    right++;
                }
            }
            else//如果为1
            {
                //进窗口
                right++;
            }
            //更新结果
            ret = max(ret, right - left);
        }
        return ret;
    }
};

我的思路:

  1. 维护窗口内0的计数zero
  2. zero > k时,左移left直到zero ≤ k
  3. 窗口大小即为当前有效区间长度

我的笔记:

  1. 贪心思想:尽可能保留更多1,通过翻转0扩展窗口
  2. 时间复杂度O(n),空间复杂度O(1)
  3. 关键优化:直接统计0的个数,避免复杂逻辑
将x减到0的最小操作数

题⽬链接:

将x减到0的最小操作数

要点:

  • 从数组两端删除元素,使得删除总和等于x
  • 转化为寻找中间连续子数组,其和等于total_sum - x

老师代码:

class Solution
{
public:
    int minOperations(vector<int>& nums, int x)
    {
        int sum = 0;
        for(int a : nums) sum += a;
        int target = sum - x;
        // 细节问题
        if(target < 0) return -1;
        int ret = -1;
        for(int left = 0, right = 0, tmp = 0; right < nums.size(); right++)
        {
            tmp += nums[right]; // 进窗⼝
            while(tmp > target) // 判断
                tmp -= nums[left++]; // 出窗⼝
            if(tmp == target) // 更新结果
                ret = max(ret, right - left + 1);
        }
        if(ret == -1) return ret;
        else return nums.size() - ret;
    }
}

老师思路:

题⽬要求的是数组「左端+右端」两段连续的、和为 x 的最短数组,信息量稍微多⼀些,不易理清思路;我们可以转化成求数组内⼀段连续的、和为 sum(nums) - x 的最⻓数组。此时,就是熟悉的「滑动窗⼝」问题了。

我的代码:

class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        //求目标值
        int target = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            target += nums[i];
        }
        target -= x;

        //计算
        int left = 0, right = 0, sum = 0, ret = -1;
        while(right < nums.size())
        {
            //进窗口
            sum += nums[right++];

            while(left < right && sum > target)//判断是否要出窗口
            {
                //出窗口
                sum -= nums[left];
                left++;
            }
            //结果更新判断
            if(sum == target)
            {
                ret = max(ret, right - left);
            }
        }
        if(ret == -1) return -1;
        else return nums.size() - ret;
    }
};

第二版

class Solution {
private:
    vector<int> ret;

public:
    vector<int> findAnagrams(string s, string p) {
        int hash1[26] = { 0 };
        for(auto e : p) hash1[e - 'a']++;

        int hash2[26] = { 0 };
        int left = 0, right = 0, count = 0;
        while(right < s.size())
        {
            hash2[s[right] - 'a']++;
            if(hash2[s[right] - 'a'] <= hash1[s[right] - 'a']) count++;//维护count
            
            //这里不需要while循环,因为窗口大小是固定的,如果进入if语句只能是大了一个数
            if(right - left + 1 > p.size())
            {
                if(hash2[s[left] - 'a'] <= hash1[s[left] - 'a']) count--;
                hash2[s[left] - 'a']--;
                left++;
            }
            if(count == p.size()) ret.push_back(left);
            right++;
        } 
        return ret;
    }
};

我的思路:

  1. 计算目标值target = sum(nums) - x
  2. 滑动窗口寻找和为target的最长子数组
  3. 最小操作数为总长度 - 最长子数组长度

我的笔记:

  1. 虽然题目中没有说这个数组的数都是正整数,但我们可以从题目下面的提示可以看到,其实这个数组的数都是正整数,因此我们就会想到单调性;又由于相当于找连续子数组,所以就可以用滑动窗口的方法

    做题时一定要看完题目

  2. 正难则反:如果一个问题从正面难解决,我们就可以反过来看一下

  3. 逆向思维:将两端删除问题转化为中间子数组问题

  4. 边界处理:当target < 0时直接返回-1

  5. 关键代码:ret = max(ret, right - left)

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

题⽬链接:

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

要点:

  • 寻找s中包含p所有字符排列的子串
  • 固定窗口大小为p的长度,维护字符频率表

老师代码:(没怎么听懂)

class Solution
{
public:
    vector<int> findAnagrams(string s, string p)
    {
        vector<int> ret;
        
        int hash1[26] = { 0 }; // 统计字符串 p 中每个字符出现的个数
        for(auto ch : p) hash1[ch - 'a']++;													//注意
        
        int hash2[26] = { 0 }; // 统计窗⼝⾥⾯的每⼀个字符出现的个数
        int m = p.size();
        
        for(int left = 0, right = 0, count = 0; right < s.size(); right++)//count用于计算滑动窗口中的元素个数
        {
            char in = s[right];
            // 进窗⼝ + 维护 count
            if(++hash2[in - 'a'] <= hash1[in - 'a']) count++;
            if(right - left + 1 > m) // 判断
            {
                char out = s[left++];
                // 出窗⼝ + 维护 count
                if(hash2[out - 'a']-- <= hash1[out - 'a']) count--;
            }
            // 更新结果
            if(count == m) ret.push_back(left);//当滑动窗口的元素个数与p元素个数一致时
        }
        return ret;
    }
}

老师思路:老师的方法

  • 因为字符串 p 的异位词的⻓度⼀定与字符串 p 的⻓度相同,所以我们可以在字符串 s 中构造⼀个⻓度为与字符串 p 的⻓度相同的滑动窗⼝,并在滑动中维护窗⼝中每种字⺟的数量
  • 当窗⼝中每种字⺟的数量与字符串 p 中每种字⺟的数量相同时,则说明当前窗⼝为字符串 p 的异位词
  • 因此可以⽤两个⼤⼩为 26 的数组来模拟哈希表,⼀个来保存 s 中的⼦串每个字符出现的个数,另⼀个来保存 p 中每⼀个字符出现的个数。这样就能判断两个串是否是异位词。
  • 在更新结果时我们需要比较两个哈希表是否相同,可以用我的代码中的硬比较方法(遍历),但那种方法不能应对哈希表比较长的情况,老师的方法是利用count来统计窗口中有效字符个数,如果有效字符个数与p元素个数一致时 这里我不好描述,一定要去看视频

我的代码:

class Solution {
private:
    vector<int> ret;

public:
    //用于比较两个数组是否相同
    bool compare(int* arrs, int* arrp)
    {
        for(int i = 0; i < 26; i++)
        {
            if(arrp[i] != arrs[i])
            {
                return false;
            }
        }
        return true;
    }
    //用于判断是否应该执行出窗口
    bool compare2(int* arrs, int* arrp)
    {
        for(int i = 0; i < 26; i++)
        {
            //如果p中没有但s中有,或s中的某个字符多余p中的数量
            //返回真,执行出窗口
            if((arrp[i] == 0 && arrs[i] != 0) ||
            arrp[i] < arrs[i])
            {
                return true;
            }
        }
        return false;
    }

    vector<int> findAnagrams(string s, string p) {
        int arrp[26] = { 0 };
        int arrs[26] = { 0 };
        for(int i = 0; i < p.size(); i++)
        {
            arrp[p[i] - 'a']++;
        }
        
        int left = 0, right = 0;
        while(right < s.size())
        {
            arrs[s[right++] - 'a']++;//进窗口
            while(left < right && compare2(arrs, arrp))//判断出窗口条件
            {
                arrs[s[left++] - 'a']--;//出窗口
            }
            if(compare(arrs, arrp))//判断更新结果的条件
            {
                ret.push_back(left);
            }
        }
        return ret;
    }
};

我的思路:

  1. 用哈希表hash1记录p的字符频率
  2. 滑动窗口维护hash2记录当前窗口字符频率
  3. 通过count变量统计有效字符个数,避免全表比较

我的笔记:

  1. 一定要注意字符串中的字符的ASCII码表对应的数组下标的值,对于数组下标要减去一个’a’才是从0开始的对应位置
  2. 我的思路是利用滑动窗口,每进窗口一个字符判断一下两个数组(哈希表)的元素关系,------硬比较
    • 如果p中没有而s中有,则s的滑动窗口中进入了p中没有的字母,我们需要出窗口
    • 如果arrs[i] > arrp[i] 则说明i这个位置的字母多了,我们需要出窗口,
    • 当这两个数组的元素个数一致时说明找到了,就返回left;
  3. 老师的方法更容易理解一些,因为这次的窗口大小是固定的 要学会利用这个特性
  4. 固定窗口大小:right - left + 1 == p.size()
  5. 哈希表优化:仅当字符频率匹配时更新count
  6. 关键代码:if(++hash2[in] <= hash1[in]) count++

注意事项总结

C++语法相关

  1. 数组初始化:int hash[128] = {0}需显式初始化
  2. 字符处理:s[right] - 'a'将字符映射到0-25的索引
  3. 边界检查:right < nums.size()避免越界

算法思路相关

  1. 单调性利用:元素为正数时,窗口和具有单调性,适合滑动窗口
  2. 逆向思维:如“将x减到0”转化为求中间子数组问题
  3. 哈希表优化:用count替代全表比较,时间复杂度从O(26n)降至O(n)
  4. 固定窗口:处理异位词时,窗口大小固定为p的长度,简化逻辑
  5. 去重技巧:通过跳跃移动左指针(如无重复字符问题),减少无效计算

位置的字母多了,我们需要出窗口,

  • 当这两个数组的元素个数一致时说明找到了,就返回left;
  1. 老师的方法更容易理解一些,因为这次的窗口大小是固定的 要学会利用这个特性
  2. 固定窗口大小:right - left + 1 == p.size()
  3. 哈希表优化:仅当字符频率匹配时更新count
  4. 关键代码:if(++hash2[in] <= hash1[in]) count++

注意事项总结

C++语法相关

  1. 数组初始化:int hash[128] = {0}需显式初始化
  2. 字符处理:s[right] - 'a'将字符映射到0-25的索引
  3. 边界检查:right < nums.size()避免越界

算法思路相关

  1. 单调性利用:元素为正数时,窗口和具有单调性,适合滑动窗口
  2. 逆向思维:如“将x减到0”转化为求中间子数组问题
  3. 哈希表优化:用count替代全表比较,时间复杂度从O(26n)降至O(n)
  4. 固定窗口:处理异位词时,窗口大小固定为p的长度,简化逻辑
  5. 去重技巧:通过跳跃移动左指针(如无重复字符问题),减少无效计算

相关文章:

  • Elasticsearch:设置向量搜索
  • CentOS安装sshpass工具-自动化SSH密码认证
  • vue2中引入elementui
  • node-ddk,electron,截屏封装(js-web-screen-shot)
  • 【蓝桥杯】每日连续 Day9 前缀和
  • Xinference安装、使用详细笔记
  • 数据库原理13
  • tcl语法中的命令
  • </mirrorOf> Maven
  • 零基础入门网络爬虫第5天:Scrapy框架
  • 嵌入式驱动开发方向的基础强化学习计划
  • 【监控系列】ELK
  • 《水上安全》杂志社水上安全编辑部水上安全杂志2025年第3期目录
  • 持续集成与持续交付:这里有从开发到部署的全流程优化
  • Linux 基础入门操作 第十二章 TINY Web 服务器
  • MyBatis StatementHandler是如何创建 Statement 对象的? 如何执行 SQL 语句?
  • mac怎么安装pycharm?
  • 【加密社】币圈合约交易量监控,含TG推送
  • 简单描述一下,大型语言模型简史
  • 内网穿透的应用-本地部署ChatTTS教程:Windows搭建AI语音合成服务的全流程配置
  • 160名老人报旅行团被扔服务区?张家界官方通报
  • 郭继孚被撤销全国政协委员资格,此前为北京交通发展研究院长
  • 王沪宁主持召开全国政协主席会议
  • 市场监管总局:2024年查办商标、专利等领域违法案件4.4万件
  • 日趋活跃!2024年我国数据生产总量同比增长25%
  • 王星昊再胜连笑,夺得中国围棋天元赛冠军