滑动窗口五题通关:从最小覆盖子串到水果成篮(Python + C++)

滑动窗口五题通关:从最小覆盖子串到水果成篮(Python + C++)

滑动窗口是处理子数组/子串问题的利器,尤其适合"连续"、"最长/最短"、"包含特定字符"等场景。本文整理了5道经典题目,每道题包含:题目描述、解题思路、图解(文本示意)、Python代码、C++代码、复杂度分析


📌 题目清单

题号 题目 核心考点
209 长度最小的子数组 滑动窗口(和 ≥ target)
904 水果成篮 最多两种字符的最长子串
76 最小覆盖子串 滑动窗口 + 哈希表(hard,高频)
438 找到字符串中所有字母异位词 固定长度滑动窗口 + 计数
567 字符串的排列 同438,判断异位词是否存在

1. 长度最小的子数组(LeetCode 209)

题目描述

给定一个含有 n 个正整数的数组和一个正整数 target。找出该数组中满足其和 ≥ target 的长度最小的连续子数组,并返回其长度。如果不存在,返回 0

示例

输入:target = 7, nums = [2,3,1,2,4,3] → 输出:2(子数组 [4,3]

解题思路

  • 使用双指针维护滑动窗口 [left, right],初始 left = 0
  • 移动右指针 right 扩大窗口,累加 sum
  • sum >= target 时,记录窗口长度,然后移动左指针缩小窗口,直到 sum < target
  • 每次缩小窗口时更新最小长度。

图解

复制代码
nums = [2,3,1,2,4,3], target=7
right=0 sum=2 <7 → right=1 sum=5 → right=2 sum=6 → right=3 sum=8 ≥7 → minLen=4, left=1 sum=6
right=4 sum=10 ≥7 → minLen=4, left=2 sum=7 ≥7 → minLen=3, left=3 sum=5
right=5 sum=8 ≥7 → minLen=3, left=4 sum=4 → left=5 sum=0 结束 → minLen=2? 检查过程遗漏: 当right=4, sum=10, left=2时窗口[2,3,1,2]长度4, 但实际上当right=5, left=4时窗口[4,3]长度2,需完整模拟。最终最小为2。

Python代码

python 复制代码
def minSubArrayLen(target, nums):
    left = 0
    total = 0
    min_len = float('inf')
    for right in range(len(nums)):
        total += nums[right]
        while total >= target:
            min_len = min(min_len, right - left + 1)
            total -= nums[left]
            left += 1
    return min_len if min_len != float('inf') else 0

C++代码

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

复杂度分析

  • 时间复杂度:O(n),每个元素最多被加入和移出窗口各一次。
  • 空间复杂度:O(1)。

2. 水果成篮(LeetCode 904)

题目描述

你正在探访一个农场,农场有一排果树(用整数数组 fruits 表示,fruits[i] 是第 i 棵树上的水果种类)。你有两个篮子,每个篮子只能装一种类型的水果,且每个篮子能装任意数量。求最多能采摘多少棵树的果实(必须连续采摘,且最多两种类型)。

示例

输入:fruits = [1,2,1] → 输出:3

输入:fruits = [0,1,2,2] → 输出:3([1,2,2] 或 [0,1,2] 中取最长连续最多两种)

输入:fruits = [1,2,3,2,2] → 输出:4([2,3,2,2])

解题思路

  • 滑动窗口,维护窗口内水果种类的计数(哈希表)。
  • 当种类超过2种时,移动左指针缩小窗口,直到种类 ≤2。
  • 记录窗口的最大长度。

图解

复制代码
fruits = [1,2,3,2,2]
right=0: {1:1} maxLen=1
right=1: {1:1,2:1} maxLen=2
right=2: {1:1,2:1,3:1} 种类3>2 → 移动left, 移除1, 窗口为[2,3] {2:1,3:1} maxLen=2
right=3: {2:2,3:1} maxLen=3
right=4: {2:3,3:1} maxLen=4

Python代码

python 复制代码
def totalFruit(fruits):
    count = {}
    left = 0
    max_len = 0
    for right, fruit in enumerate(fruits):
        count[fruit] = count.get(fruit, 0) + 1
        while len(count) > 2:
            count[fruits[left]] -= 1
            if count[fruits[left]] == 0:
                del count[fruits[left]]
            left += 1
        max_len = max(max_len, right - left + 1)
    return max_len

C++代码

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        unordered_map<int, int> count;
        int left = 0, maxLen = 0;
        for (int right = 0; right < fruits.size(); ++right) {
            count[fruits[right]]++;
            while (count.size() > 2) {
                count[fruits[left]]--;
                if (count[fruits[left]] == 0) count.erase(fruits[left]);
                left++;
            }
            maxLen = max(maxLen, right - left + 1);
        }
        return maxLen;
    }
};

复杂度分析

  • 时间复杂度:O(n)。
  • 空间复杂度:O(1)(因为最多3种水果,哈希表大小常数)。

3. 最小覆盖子串(LeetCode 76)

题目描述

给你一个字符串 s、一个字符串 t。返回 s 中涵盖 t 所有字符的最小子串。如果不存在,返回空字符串 ""

注意:t 中可能包含重复字符,子串必须包含 t 中每个字符的相同频次。

示例

输入:s = "ADOBECODEBANC", t = "ABC" → 输出:"BANC"

解题思路

  • 使用两个哈希表(或数组)need 记录 t 中字符需求,window 记录当前窗口字符计数。
  • 维护变量 valid 表示当前窗口已经满足需求的字符种类数(每个字符的计数达到 need 要求)。
  • 右指针扩展窗口,当 valid == need 的种类数时,尝试左指针收缩窗口,更新最小覆盖子串。
  • 收缩直到不满足条件,继续移动右指针。

图解

复制代码
s = "ADOBECODEBANC", t = "ABC"
need: A:1,B:1,C:1
right=0: A -> window{A:1} valid=1 (A满足)
right=1: D
right=2: O
right=3: B -> window{A:1,B:1} valid=2
right=4: E
right=5: C -> window{A:1,B:1,C:1} valid=3 满足,记录子串[0,5]="ADOBEC", 收缩left:
left=0移除A -> window{A:0} valid=2, 子串[1,5]="DOBEC" 不满足,继续扩展
right=6: O
right=7: D
right=8: E
right=9: B -> window{B:2,C:1} valid=2 (缺A)
right=10: A -> window{A:1,B:2,C:1} valid=3 满足,收缩left:
left=1移除D...最终得到"BANC"

Python代码

python 复制代码
def minWindow(s, t):
    from collections import defaultdict
    need = defaultdict(int)
    for c in t:
        need[c] += 1
    window = defaultdict(int)
    left = 0
    valid = 0
    start = 0
    min_len = float('inf')
    for right, c in enumerate(s):
        if c in need:
            window[c] += 1
            if window[c] == need[c]:
                valid += 1
        while valid == len(need):
            if right - left + 1 < min_len:
                min_len = right - left + 1
                start = left
            d = s[left]
            left += 1
            if d in need:
                if window[d] == need[d]:
                    valid -= 1
                window[d] -= 1
    return "" if min_len == float('inf') else s[start:start+min_len]

C++代码

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char, int> need, window;
        for (char c : t) need[c]++;
        int left = 0, valid = 0;
        int start = 0, minLen = INT_MAX;
        for (int right = 0; right < s.size(); ++right) {
            char c = s[right];
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c]) valid++;
            }
            while (valid == need.size()) {
                if (right - left + 1 < minLen) {
                    minLen = right - left + 1;
                    start = left;
                }
                char d = s[left];
                left++;
                if (need.count(d)) {
                    if (window[d] == need[d]) valid--;
                    window[d]--;
                }
            }
        }
        return minLen == INT_MAX ? "" : s.substr(start, minLen);
    }
};

复杂度分析

  • 时间复杂度:O(n + m),其中 n 为 s 长度,m 为 t 长度。
  • 空间复杂度:O(Σ),字符集大小(常数,如 ASCII 128)。

4. 找到字符串中所有字母异位词(LeetCode 438)

题目描述

给定两个字符串 sp,找到 s 中所有 p 的异位词的子串,返回这些子串的起始索引。异位词指字母相同但排列不同的字符串。

示例

输入:s = "cbaebabacd", p = "abc" → 输出:[0,6]

解释:索引0的子串 "cba" 是异位词,索引6的子串 "bac" 是异位词。

解题思路

  • 固定窗口大小 len(p),用滑动窗口在 s 上滑动。
  • 维护两个计数数组(或哈希表),一个为 p 的字符计数,另一个为当前窗口的字符计数。
  • 当两个计数相等时,记录窗口起始索引。
  • 优化:使用 diff 变量记录不同字符的数量,但固定窗口大小简单方法直接比较计数数组即可(因为长度固定且字符集有限)。

图解

复制代码
s = "cbaebabacd", p = "abc"
p计数: a:1,b:1,c:1
窗口[0,2]="cba": c:1,b:1,a:1 匹配 → 记录0
窗口右移[1,3]="bae": b:1,a:1,e:1 不匹配
... 直到[6,8]="bac": b:1,a:1,c:1 匹配 → 记录6

Python代码

python 复制代码
def findAnagrams(s, p):
    from collections import Counter
    need = Counter(p)
    window = Counter()
    res = []
    left = 0
    for right, ch in enumerate(s):
        window[ch] += 1
        if right - left + 1 > len(p):
            window[s[left]] -= 1
            if window[s[left]] == 0:
                del window[s[left]]
            left += 1
        if window == need:
            res.append(left)
    return res

C++代码

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> need(26, 0), window(26, 0);
        for (char c : p) need[c - 'a']++;
        vector<int> res;
        int left = 0;
        for (int right = 0; right < s.size(); ++right) {
            window[s[right] - 'a']++;
            if (right - left + 1 > p.size()) {
                window[s[left] - 'a']--;
                left++;
            }
            if (window == need) res.push_back(left);
        }
        return res;
    }
};

复杂度分析

  • 时间复杂度:O(n + m),其中 n 为 s 长度,m 为 p 长度。
  • 空间复杂度:O(1)(计数数组固定大小)。

5. 字符串的排列(LeetCode 567)

题目描述

给定两个字符串 s1s2,判断 s2 是否包含 s1 的排列(即 s1 的某个排列是 s2 的子串)。返回 truefalse

示例

输入:s1 = "ab", s2 = "eidbaooo" → 输出:true("ba" 是子串)

输入:s1 = "ab", s2 = "eidboaoo" → 输出:false

解题思路

  • 与 438 完全相同,只需判断是否存在任意一个窗口匹配即可。
  • 滑动窗口大小固定为 len(s1),滑动过程中比较窗口计数与 s1 计数是否相等。

图解

复制代码
s1 = "ab", s2 = "eidbaooo"
need: a:1,b:1
窗口[0,1]="ei" 不匹配
[1,2]="id"
[2,3]="db"
[3,4]="ba" 匹配 → 返回 true

Python代码

python 复制代码
def checkInclusion(s1, s2):
    from collections import Counter
    need = Counter(s1)
    window = Counter()
    left = 0
    for right, ch in enumerate(s2):
        window[ch] += 1
        if right - left + 1 > len(s1):
            window[s2[left]] -= 1
            if window[s2[left]] == 0:
                del window[s2[left]]
            left += 1
        if window == need:
            return True
    return False

C++代码

cpp 复制代码
class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        vector<int> need(26, 0), window(26, 0);
        for (char c : s1) need[c - 'a']++;
        int left = 0;
        for (int right = 0; right < s2.size(); ++right) {
            window[s2[right] - 'a']++;
            if (right - left + 1 > s1.size()) {
                window[s2[left] - 'a']--;
                left++;
            }
            if (window == need) return true;
        }
        return false;
    }
};

复杂度分析

  • 时间复杂度:O(n + m),其中 n 为 s2 长度,m 为 s1 长度。
  • 空间复杂度:O(1)。

🎯 总结

题目 核心技巧 时间复杂度 空间复杂度
209. 长度最小的子数组 不定长窗口,和≥target时收缩 O(n) O(1)
904. 水果成篮 不定长窗口,最多两种字符 O(n) O(1)
76. 最小覆盖子串 不定长窗口 + 哈希表计数 O(n+m) O(Σ)
438. 字母异位词 固定长度窗口 + 计数比较 O(n+m) O(1)
567. 字符串的排列 同438,判断存在性 O(n+m) O(1)

滑动窗口的核心在于:何时扩大右边界,何时缩小左边界。对于固定长度窗口,每次移动一步并维护计数;对于不定长窗口,通常需要满足某种条件后尝试收缩。多练习即可掌握。

相关推荐
Xpower 171 小时前
OpenClaw近一月版本更替讲解
人工智能·学习·算法
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年5月11日
大数据·人工智能·python·信息可视化·自然语言处理
Ulyanov1 小时前
《从质点到位姿:基于Python与PyVista的导弹制导控制全栈仿真》: 刚体觉醒——6-DOF刚体动力学、四元数与全姿态解算
开发语言·人工智能·python·算法·系统仿真·雷达电子对抗仿真
chase。1 小时前
【学习笔记】BifrostUMI 论文全面解析
人工智能·笔记·学习
Sylvia33.1 小时前
足球数据API接入实战:从认证到实时比分推送的完整指南
java·开发语言·前端·c++·python
_小郑有点困了1 小时前
学习Python基础语法及使用
前端·python·学习
Chloeis Syntax1 小时前
JavaEE初阶学习日记(1)---线程和进程
java·开发语言·学习·线程·javaee
国强_dev1 小时前
如何提升canal吞吐量
java·大数据·python
时空自由民.1 小时前
C/C++ volatile关键字原理及应用介绍
java·c语言·c++