滑动窗口算法

欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到say−fall的文章

🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。


前言:

本文系统讲解双指针与滑动窗口算法,从暴力枚举出发,逐步推导出滑动窗口的核心思想,并配合多道 LeetCode 例题加深理解。掌握这一技巧,能将大量 O(n²) 的问题优化到 O(n)。


文章目录


正文:

算法思想

滑动窗口算法,实际上其本质就是同向双指针 ,同向双指针之间夹有 有用区间 或者 无用区间 ,这个时候区间的某种数据就是结果。

双指针按形态可以分为三类:

类型 方向 典型场景
对撞指针 相向 有序数组两端夹逼
快慢指针 同向不同速 链表环、中点
滑动窗口 同向同速,窗口伸缩 连续子数组/子串

滑动窗口的核心步骤:

  1. 进窗口:右指针右移,新元素加入窗口
  2. 判断:检查窗口是否满足/违反条件
  3. 出窗口:左指针右移,旧元素移出窗口,更新结果
  4. 重复上述过程直到右指针越界

长度最小的子数组

题意 :给定正整数数组 nums 和目标值 target,找长度最小的连续子数组,使其元素之和 ≥ target,返回其长度,不存在则返回 0。

解法一:多层循环暴力枚举

本题可以直接双层循环+sum记录大小

cpp 复制代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
    int sum = 0,ret = INT_MAX;
    for(int left = 0;left < nums.size();left++)	
    {
        sum = 0;
	    for(int right = left;right < nums.size();right++)
	    {
		    sum += nums[right];
		    if(sum >= target) 
            {
                ret = min(right - left + 1,ret);
                break;
            }
	    }
    }
    if(ret == INT_MAX) return 0;
    return ret;
    }
};

而很显然,这种方法太慢了,既然已经每增加一个数字sum都是变大的,利用单调性,我们能不能不让right每次都重复移动,而是需要时候向后移动:这就是滑动窗口算法的核心。

解法二:滑动窗口

  1. 进窗口
  2. 判断
  3. ---->出窗口,更新结果
  4. ---->继续进窗口
cpp 复制代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int left = 0,right = 0,sum = 0, ret = 0;
        while(right < nums.size())
        {
            sum += nums[right];
            while(sum >= target)
            {//判断,出窗口
                if(ret == 0 || ret > right - left + 1) ret = right - left + 1;
                sum -= nums[left];
                left++;
            }
            //进窗口
            right++;
        }
        return ret;
    }
};

💡 为什么正确? 数组全为正整数,sum 具有单调性:right 右移 sum 只增,left 右移 sum 只减。因此每个元素最多被 left 和 right 各访问一次,时间复杂度 O(n)


无重复字符的最长字串

题意 :给定字符串 s,找其中不含重复字符的最长子串的长度。

这题的思路明显和上一题是类似的,只不过没有 target 作为出窗口的判断条件,而是判断字串中有没有重复字符,我们可以使用一个类哈希表来处理。

滑动窗口

cpp 复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) 
    {
        int hash[128] = {0};
        int left = 0 ,right = 0;
        int ret = 0;
        while(right < s.size())
        {
            hash[s[right]]++;//进窗口
            while(hash[s[right]] > 1)//判断:出现重复字符
            {
                hash[s[left]]--;//出窗口
                left++;
            }
            ret = max(ret,right - left + 1);//更新结果
            right++;
        }
        return ret;
    }
};

💡 关键点 :用 hash[128] 充当字符频次表(ASCII 共 128 个字符),hash[s[right]] > 1 说明 s[right] 在窗口内重复出现,此时持续收缩左边直到不重复为止。


最大连续1的个数Ⅲ

题意 :给定二进制数组 nums 和整数 k,最多可以将 k 个 0 翻转为 1,返回最长连续 1 的子数组长度。

转化:子数组中 0 的个数 ≤ k,求满足条件的最长子数组。取消翻转操作,因为题目没有要求修改原数组。

解法一:暴力枚举 和 zero计数器

cpp 复制代码
// 伪代码:
for(int left = 0;left < n;left++)	
{
	for(right = left;right < n;right++)
	{
		// 统计 [left, right] 内 0 的个数
		// 若 zero <= k,更新结果
	}
}

时间复杂度 O(n²),数据量大时超时。

解法二:滑动指针优化

cpp 复制代码
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int left = 0,right = 0,ret = 0,n = nums.size(),zero = 0;
        for(left = 0,right = 0;right < n;right++)//进窗口
        {
            if(nums[right] == 0)
            {
                zero++;
            }
            while(zero > k)//判断:0 的个数超出 k
            {
                if(nums[left] == 0) zero--;
                left++;//出窗口
            }
            ret = max(ret,right - left + 1);//更新结果
        }
        return ret;
       }
};

💡 思路 :窗口内维护 0 的计数器 zero,一旦 zero > k 就收缩左边。每次 right 右移后更新最大长度。


将x减到零的最小操作数

题意:每次操作可以从数组最左或最右取一个数,使 x 减去该数,求使 x 恰好为 0 的最少操作数。

转化 :从两端取数使和为 x,等价于找中间连续子数组使和为 sum - x,且该子数组长度最大(操作数 = n - 子数组长度)。

cpp 复制代码
class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        int left = 0, right = 0, n = nums.size();
        int sum = 0;
        for (auto e : nums)
        {
            sum += e;
        }
        int t = sum - x;
        if (t < 0) return -1;   // 总和都不够,无解
        if (t == 0) return n;   // 整个数组就是答案
        int ret = -1;
        sum = 0;
        while (right < n)
        {
            sum += nums[right];             // 进窗口
            while (sum > t) sum -= nums[left++]; // 出窗口
            if (sum == t) ret = max(right - left + 1, ret); // 更新结果
            right++;
        }
        if (ret == -1) return -1;
        return n - ret;
    }
};

💡 正难则反:直接模拟从两端取数很复杂,转化为求中间最长子数组是滑动窗口的经典套路。


水果成篮

题意:给定水果类型数组,有两个篮子,每个篮子只能装一种水果,从某棵树开始连续摘,求最多能摘多少水果。

转化:找最长子数组,使其中水果种类 ≤ 2。

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int left = 0, right = 0, ret = 0, kinds = 0;
        int n = fruits.size();
        int hash[100001] = { 0 };
        for (right = 0;right < n;right++)
        {
            if (hash[fruits[right]] == 0) kinds++; // 新种类进入
            hash[fruits[right]]++;                  // 进窗口
            while (kinds > 2)                       // 判断:超过2种
            {
                hash[fruits[left]]--;
                if (hash[fruits[left]] == 0) kinds--; // 种类消失
                left++;                               // 出窗口
            }
            ret = max(ret, right - left + 1);
        }
        return ret;
    }
};

💡 哈希表维护种类数kinds 记录窗口内水果种类,hash 记录每种水果数量。种类超过 2 时收缩左边,直到某种水果数量归零(种类减少)。


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

题意 :给定字符串 sp,找 s 中所有 p 的异位词的起始索引。

思路 :固定窗口大小为 p.size(),用 count 记录窗口内有效字符数(频次不超过 p 中对应字符数的字符),count == p.size() 时即为异位词。

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int left = 0, right = 0;
        vector<int> ans;
        int count = 0;
        int hash1[26] = {0};  // p 的字符频次
        int hash2[26] = {0};  // 窗口内字符频次
        int len = p.size();
        
        for (int i = 0; i < len; i++)
            hash1[p[i] - 'a']++;

        for (right = 0; right < s.size(); right++)
        {
            int in = s[right] - 'a';
            
            // 进窗口:遇到 p 中没有的字符,重置窗口
            if (hash1[in] == 0)
            {
                left = right + 1;
                count = 0;
                memset(hash2, 0, sizeof(hash2));
                continue;
            }
            hash2[in]++;
            if (hash2[in] <= hash1[in]) count++; // 有效字符数+1

            // 出窗口:窗口超过 len
            if (right - left + 1 > len)
            {
                int out = s[left] - 'a';
                hash2[out]--;
                left++;
                if (hash2[out] < hash1[out]) count--; // 有效字符数-1
            }

            // 更新结果
            if (count == len)
                ans.push_back(left);
        }
        return ans;
    }
};

💡 count 的含义count 统计窗口内"有效"字符数,即频次不超过 p 中对应字符频次的字符总数。count == len 说明窗口恰好是 p 的一个异位词。


串联所有单词的字串

题意 :给定字符串 s 和单词列表 words(每个单词等长),找 s 中所有由 words 中所有单词串联而成的子串的起始位置。

思路 :单词长度为 len,以 0, 1, ..., len-1 为起点分别做滑动窗口,步长为 len,窗口大小固定为 n * len

cpp 复制代码
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> ans;
        int count = 0;

        unordered_map<string, int> hash1;
        int n = words.size();
        for (auto s : words) hash1[s]++;

        unordered_map<string, int> hash2;
        int len = words[0].size();
        
        for (int i = 0; i < len; i++) // 枚举起点,共 len 种
        {
            hash2.clear();
            count = 0;
            for (int left = i, right = i + len; right <= (int)s.size(); right += len)
            {
                // 进窗口
                string in = s.substr(right - len, len);
                hash2[in]++;
                if (hash1.count(in) && hash2[in] <= hash1[in]) count++;

                // 出窗口:窗口超过 n*len
                if (right - left > n * len)
                {
                    string out = s.substr(left, len);
                    left += len;
                    hash2[out]--;
                    if (hash1.count(out) && hash2[out] < hash1[out]) count--;
                }

                // 更新结果
                if (count == n) ans.push_back(left);
            }
            count = 0;
            hash2.clear();
        }
        return ans;
    }
};

💡 为什么枚举 len 个起点? 因为单词长度固定为 len,所有可能的起点只有 0 ~ len-1len 种,每种起点独立做一次滑动窗口即可覆盖所有情况。


总结

复制代码
双指针
├── 对撞指针  → 有序数组,两端夹逼(两数之和、三数之和)
├── 快慢指针  → 链表,环/中点/倒数第k个
└── 滑动窗口  → 连续子数组/子串,窗口伸缩
    ├── 可变窗口  → 最小子数组、最长无重复子串、最大连续1
    └── 固定窗口  → 字母异位词、串联所有单词的子串

滑动窗口的本质 :利用数据的单调性 ,避免重复枚举。每个元素最多被 left 和 right 各访问一次,时间复杂度 O(n) ,空间复杂度 O(1)O(k)(k 为字符集大小)。

套路口诀

进窗口 → 判断 → 出窗口 → 更新结果


  • 本节完...
相关推荐
落羽的落羽2 小时前
【算法札记】练习 | Week1
linux·服务器·c++·人工智能·python·算法·机器学习
qq_454245032 小时前
图数据标准化与智能去重框架:设计与实现解析
数据结构·架构·c#·图论
c++圈来了个新人2 小时前
C++类和对象(上)
c语言·开发语言·数据结构·c++·考研
人道领域2 小时前
【LeetCode刷题日记】15.三数之和(梦破碎的地方)
算法·leetcode·面试
️是782 小时前
信息奥赛一本通(4005:【GESP2306一级】时间规划)
数据结构·c++·算法
tankeven2 小时前
HJ174 交换到最大
c++·算法
AI科技星2 小时前
基于v≡c第一性原理:密度的本质与时空动力学
人工智能·学习·算法·机器学习·数据挖掘
kishu_iOS&AI2 小时前
机器学习 —— 聚类算法
人工智能·算法·机器学习·聚类
hope_wisdom2 小时前
C/C++数据结构之树
数据结构·c++·二叉树·