1. 滑动窗口算法核心思想解析
滑动窗口是解决数组/字符串子区间问题的高效算法范式,特别适合处理"满足某条件的最长子串/子数组"类问题。其本质是通过维护一个动态变化的窗口来避免重复计算,将暴力解法O(n²)的时间复杂度优化至O(n)。
在C++实现中,窗口通常用两个指针(left, right)表示区间[left, right]。算法框架包含三个关键操作:
- 右指针移动扩展窗口(寻找可行解)
- 左指针移动收缩窗口(优化可行解)
- 窗口状态更新与结果记录
cpp复制int left = 0, right = 0;
while (right < s.size()) {
// 扩展窗口
window.add(s[right]);
right++;
while (valid(window)) {
// 收缩窗口
window.remove(s[left]);
left++;
}
}
2. 特殊题型分类与解题模板
2.1 固定长度窗口问题
典型特征:要求找到长度为k的子数组满足特定条件(如最大平均值)。
解题模板:
cpp复制vector<int> findSubarray(vector<int>& nums, int k) {
int sum = 0, maxSum = INT_MIN;
int left = 0;
vector<int> res;
for (int right = 0; right < nums.size(); ++right) {
sum += nums[right];
if (right >= k - 1) {
if (sum > maxSum) {
maxSum = sum;
res = vector<int>(nums.begin() + left, nums.begin() + right + 1);
}
sum -= nums[left++];
}
}
return res;
}
2.2 可变长度窗口问题
典型特征:要求找到满足条件的最长/最短子数组(如不含重复字符的最长子串)。
解题模板:
cpp复制int variableWindow(string s) {
unordered_map<char, int> window;
int left = 0, maxLen = 0;
for (int right = 0; right < s.size(); ++right) {
window[s[right]]++;
while (window.size() > k) { // k为约束条件
if (--window[s[left]] == 0) {
window.erase(s[left]);
}
left++;
}
maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
2.3 多条件组合窗口问题
典型特征:需要同时满足多个约束条件(如包含所有目标字符的最短子串)。
解题模板:
cpp复制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, len = 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 < len) {
start = left;
len = right - left + 1;
}
char d = s[left++];
if (need.count(d)) {
if (window[d] == need[d]) valid--;
window[d]--;
}
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
3. 典型例题深度剖析
3.1 最长无重复子串(LeetCode 3)
问题描述: 给定字符串s,找出不含有重复字符的最长子串长度。
解法分析:
- 使用哈希表记录字符最后出现位置
- 当遇到重复字符时,快速跳转左指针
- 时间复杂度O(n),空间复杂度O(min(m,n)),m为字符集大小
cpp复制int lengthOfLongestSubstring(string s) {
unordered_map<char, int> lastPos;
int left = 0, maxLen = 0;
for (int right = 0; right < s.size(); ++right) {
char c = s[right];
if (lastPos.count(c) && lastPos[c] >= left) {
left = lastPos[c] + 1;
}
lastPos[c] = right;
maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
3.2 最小覆盖子串(LeetCode 76)
问题描述: 在字符串s中找到包含字符串t所有字符的最小子串。
解法分析:
- 使用两个哈希表分别记录需要和当前窗口的字符计数
- valid变量统计满足条件的字符个数
- 收缩窗口时更新最小长度和起始位置
cpp复制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, len = 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 < len) {
start = left;
len = right - left + 1;
}
char d = s[left++];
if (need.count(d)) {
if (window[d] == need[d]) valid--;
window[d]--;
}
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
3.3 字符串排列(LeetCode 567)
问题描述: 判断字符串s2是否包含s1的排列。
解法分析:
- 固定长度窗口等于s1长度
- 比较窗口内字符频率是否匹配
- 使用数组替代哈希表提升性能
cpp复制bool checkInclusion(string s1, string s2) {
vector<int> need(26, 0), window(26, 0);
for (char c : s1) need[c - 'a']++;
int left = 0, count = 0;
for (int right = 0; right < s2.size(); ++right) {
int idx = s2[right] - 'a';
window[idx]++;
if (right - left + 1 == s1.size()) {
if (window == need) return true;
window[s2[left] - 'a']--;
left++;
}
}
return false;
}
4. 性能优化技巧与边界处理
4.1 哈希表优化策略
当字符集有限时(如仅小写字母),用数组替代unordered_map可显著提升性能:
cpp复制vector<int> need(26, 0), window(26, 0);
// 访问时直接使用字符ASCII码差值作为索引
window[s[right] - 'a']++;
4.2 窗口收缩条件优化
某些情况下可以记录上次合法位置来避免重复计算:
cpp复制int lastValid = -1;
while (valid == need.size()) {
lastValid = left;
// ...
}
// 后续可直接从lastValid开始检查
4.3 特殊边界情况处理
- 空字符串输入:需在函数开始处添加检查
- 目标字符串t包含重复字符:need哈希表应记录字符出现次数而非简单存在
- 无解情况:返回前检查结果是否被更新过
5. 常见错误与调试技巧
5.1 指针移动顺序错误
典型错误:先移动指针再更新窗口状态。正确顺序应为:
- 更新窗口状态
- 移动指针
- 检查条件
5.2 哈希表计数问题
当使用window[c] == need[c]判断时,注意:
- 必须先检查
need.count(c) - 自增操作要在判断之前完成
5.3 窗口大小计算误区
窗口长度应为right - left + 1而非right - left,特别是在循环开始时right和left都为0的情况。
6. 扩展应用与变种问题
6.1 最多K个不同字符的子串
通过维护哈希表大小来限制不同字符数量:
cpp复制int lengthOfLongestSubstringKDistinct(string s, int k) {
unordered_map<char, int> window;
int left = 0, maxLen = 0;
for (int right = 0; right < s.size(); ++right) {
window[s[right]]++;
while (window.size() > k) {
if (--window[s[left]] == 0) {
window.erase(s[left]);
}
left++;
}
maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
6.2 替换后最长重复字符
允许替换k次字符,求最长的重复字符子串:
cpp复制int characterReplacement(string s, int k) {
vector<int> count(26, 0);
int left = 0, maxCount = 0, maxLen = 0;
for (int right = 0; right < s.size(); ++right) {
count[s[right] - 'A']++;
maxCount = max(maxCount, count[s[right] - 'A']);
while (right - left + 1 - maxCount > k) {
count[s[left] - 'A']--;
left++;
}
maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
6.3 乘积小于K的子数组
处理数值乘积类问题时需注意:
- 乘积初始化应为1
- 处理乘积为0的特殊情况
- 收缩条件变为乘积≥k
cpp复制int numSubarrayProductLessThanK(vector<int>& nums, int k) {
if (k <= 1) return 0;
int left = 0, product = 1, count = 0;
for (int right = 0; right < nums.size(); ++right) {
product *= nums[right];
while (product >= k) {
product /= nums[left++];
}
count += right - left + 1;
}
return count;
}
在实际工程应用中,滑动窗口算法常与前缀和、单调队列等技巧结合使用。对于超大规模数据流处理,可以考虑分布式滑动窗口的实现方案。