滑动窗口五题通关:从最小覆盖子串到水果成篮(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)
题目描述
给定两个字符串 s 和 p,找到 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)
题目描述
给定两个字符串 s1 和 s2,判断 s2 是否包含 s1 的排列(即 s1 的某个排列是 s2 的子串)。返回 true 或 false。
示例 :
输入: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) |
滑动窗口的核心在于:何时扩大右边界,何时缩小左边界。对于固定长度窗口,每次移动一步并维护计数;对于不定长窗口,通常需要满足某种条件后尝试收缩。多练习即可掌握。