目录
- 滑动窗口算法详解
-
- 一、滑动窗口的基本概念
- 二、C++滑动窗口通用模板
- 三、经典题目代码解析
-
- [1. 209. 长度最小的子数组(基础模板)](#1. 209. 长度最小的子数组(基础模板))
- [2. 3. 无重复字符的最长子串](#2. 3. 无重复字符的最长子串)
- [3. 904. 水果成篮(使用哈希表)](#3. 904. 水果成篮(使用哈希表))
- [4. 438. 找到字符串中所有字母异位词(固定窗口)](#4. 438. 找到字符串中所有字母异位词(固定窗口))
- 四、滑动窗口的变体和技巧
-
- [1. 使用数组代替哈希表(效率更高)](#1. 使用数组代替哈希表(效率更高))
- [2. 计数器的使用(优化判断)](#2. 计数器的使用(优化判断))
- 五、常见错误和注意事项
-
- [1. 边界条件处理](#1. 边界条件处理)
- [2. 指针移动时机](#2. 指针移动时机)
- [3. 整数溢出](#3. 整数溢出)
- 六、滑动窗口的复杂度分析
- 七、练习题建议
滑动窗口算法详解
一、滑动窗口的基本概念
滑动窗口是一种处理连续子数组/子串问题的高效算法,通过维护一个动态的窗口(由两个指针定义),在O(n)时间内解决问题。
二、C++滑动窗口通用模板
基础模板(最小窗口问题)
cpp
int left = 0, right = 0; // 窗口的左右边界
int windowValue = 0; // 窗口的当前值(如和、种类数等)
int result = INT_MAX; // 存储最终结果
int n = nums.size(); // 数组长度
while (right < n) {
// 1. 右指针扩大窗口
windowValue += process(nums[right]); // 处理右指针元素
// 2. 当窗口满足条件时,尝试缩小窗口
while (windowConditionMet(windowValue, target)) {
// 更新结果
result = min(result, right - left + 1);
// 左指针缩小窗口
windowValue -= process(nums[left]);
left++;
}
right++;
}
return result == INT_MAX ? 0 : result; // 处理未找到的情况
三、经典题目代码解析
1. 209. 长度最小的子数组(基础模板)
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int left = 0, right = 0;
int sum = 0; // 窗口内元素的和
int minLen = INT_MAX; // 最小长度
while (right < n) {
// 1. 右指针移动,扩大窗口
sum += nums[right];
// 2. 当窗口满足条件(和>=target)
while (sum >= target) {
// 更新最小长度
minLen = min(minLen, right - left + 1);
// 左指针移动,缩小窗口
sum -= nums[left];
left++;
}
right++;
}
return minLen == INT_MAX ? 0 : minLen;
}
};
2. 3. 无重复字符的最长子串
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.size();
if (n == 0) return 0;
int left = 0, right = 0;
int maxLen = 0;
unordered_set<char> window; // 用哈希集合记录窗口内的字符
while (right < n) {
char c = s[right];
// 如果字符已存在,移动左指针直到去除重复
while (window.count(c)) {
window.erase(s[left]);
left++;
}
// 添加当前字符到窗口
window.insert(c);
// 更新最大长度
maxLen = max(maxLen, right - left + 1);
right++;
}
return maxLen;
}
};
3. 904. 水果成篮(使用哈希表)
cpp
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int n = fruits.size();
unordered_map<int, int> basket; // 篮子:水果类型 -> 数量
int left = 0, right = 0;
int maxFruits = 0;
while (right < n) {
// 1. 摘取当前水果
int fruit = fruits[right];
basket[fruit]++;
// 2. 如果篮子种类超过2,移动左指针
while (basket.size() > 2) {
int leftFruit = fruits[left];
basket[leftFruit]--;
if (basket[leftFruit] == 0) {
basket.erase(leftFruit);
}
left++;
}
// 3. 更新最大水果数
maxFruits = max(maxFruits, right - left + 1);
right++;
}
return maxFruits;
}
};
4. 438. 找到字符串中所有字母异位词(固定窗口)
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> result;
if (s.size() < p.size()) return result;
vector<int> pCount(26, 0); // p的字符频率
vector<int> windowCount(26, 0); // 窗口的字符频率
// 统计p的字符频率
for (char c : p) {
pCount[c - 'a']++;
}
// 初始化第一个窗口
for (int i = 0; i < p.size(); i++) {
windowCount[s[i] - 'a']++;
}
// 检查第一个窗口
if (windowCount == pCount) {
result.push_back(0);
}
// 滑动窗口
for (int right = p.size(); right < s.size(); right++) {
// 移除左边界字符
int left = right - p.size();
windowCount[s[left] - 'a']--;
// 添加右边界字符
windowCount[s[right] - 'a']++;
// 检查当前窗口
if (windowCount == pCount) {
result.push_back(left + 1);
}
}
return result;
}
};
四、滑动窗口的变体和技巧
1. 使用数组代替哈希表(效率更高)
cpp
// 针对小写字母问题,用数组代替unordered_map
int hash[26] = {0}; // 比unordered_map快
// 针对ASCII字符
int hash[128] = {0}; // 覆盖所有ASCII字符
2. 计数器的使用(优化判断)
cpp
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int left = 0, right = 0;
int zeroCount = 0; // 窗口内0的个数
int maxLen = 0;
while (right < nums.size()) {
// 统计0的个数
if (nums[right] == 0) {
zeroCount++;
}
// 如果0太多,移动左指针
while (zeroCount > k) {
if (nums[left] == 0) {
zeroCount--;
}
left++;
}
// 更新最大长度
maxLen = max(maxLen, right - left + 1);
right++;
}
return maxLen;
}
};
五、常见错误和注意事项
1. 边界条件处理
cpp
// 错误:忘记处理空数组
if (nums.empty()) return 0;
// 错误:忘记初始化结果
int result = 0; // 或者INT_MAX/INT_MIN
2. 指针移动时机
cpp
// 正确的顺序
while (right < n) {
// 1. 更新窗口状态
// 2. 检查条件,可能需要while循环
// 3. 更新答案
// 4. 移动右指针
}
// 错误:在更新答案前移动了右指针
3. 整数溢出
cpp
// 对于大数组,和可能溢出
long long sum = 0; // 使用long long
// 计算长度时
int length = right - left + 1; // 可能为负值
if (length < 0) length = 0;
六、滑动窗口的复杂度分析
- 时间复杂度:O(n),每个元素最多被访问两次(进窗口和出窗口各一次)
- 空间复杂度 :
- 使用哈希表:O(k),k为字符集大小
- 使用数组:O(1)或O( C ),C为常数(如26或128)
七、练习题建议
按难度递增顺序练习:
- 209.长度最小的子数组(基础)
- 3.无重复字符的最长子串(经典)
- 1004.最大连续1的个数 III(带约束)
- 904.水果成篮(种类限制)
- 438.找到字符串中所有字母异位词(固定窗口)
- 76.最小覆盖子串(综合应用)
- 30.串联所有单词的子串(困难)
掌握滑动窗口的关键是理解窗口何时扩大、何时缩小、何时更新答案这三个核心问题。通过练习这些题目,你会在面试和算法竞赛中游刃有余。