一、进阶趣味原理:会伸缩的动态窗户
上篇的固定窗口是「定死大小的玻璃窗」,只能整体平移;下篇的可变滑动窗口是「可拉伸的橡皮筋窗户」!
依旧用蜗牛场景升级理解:蜗牛的窗户可以自由变大、缩小,遇到合法元素就拉长身体(右移right),遇到非法元素就收缩尾巴(右移left),窗口大小动态变化,适配更复杂的算法题型。
核心适用场景:寻找「最长子串、最短子数组、满足条件的连续区间」等无固定长度的问题,是面试高频考点。
二、可变滑动窗口通用模板(C++)
cpp
int left = 0;
for(int right = 0; right < nums.size(); right++){
// 1. 纳入右指针元素,更新窗口状态
// 2. 窗口不合法时,循环收缩左边界
while(窗口不合法条件){
// 剔除左指针元素,更新窗口状态
left++;
}
// 3. 窗口合法,更新最优答案(最长/最短/总数)
}
三、下篇4个实战案例(逐行代码详解)
案例1:无重复字符的最长子串(面试必刷)
题目描述:给定字符串,找出其中不含有重复字符的最长子串长度
解题思路:可变窗口,右指针探索新字符,出现重复则收缩左窗口,保证窗口内无重复
cpp
#include<iostream>
#include<string>
#include<unordered_set>
#include<algorithm>
using namespace std;
int lengthOfLongestSubstring(string s) {
unordered_set<char> st; // 存储当前窗口内的字符
int left = 0, maxLen = 0;
for(int right = 0; right < s.size(); right++){
// 出现重复字符,收缩左窗口,直到无重复
while(st.find(s[right]) != st.end()){
st.erase(s[left]);
left++;
}
st.insert(s[right]); // 纳入新字符
maxLen = max(maxLen, right - left + 1); // 更新最大长度
}
return maxLen;
}
int main(){
string s = "abcabcbb";
cout << lengthOfLongestSubstring(s) << endl; // 输出3
return 0;
}
代码详解:
-
用哈希集合
st存储当前窗口所有字符,快速判断是否重复; -
右指针遍历字符,若当前字符已在集合中,说明窗口重复,持续删除左边界字符、右移左指针;
-
窗口无重复后,纳入当前字符,更新最长子串长度;
-
窗口大小动态伸缩,完美适配无固定长度的最优解查找。
案例2:最小长度子数组和(最短窗口问题)
题目描述:给定正整数数组和目标值target,找出和≥target的最短连续子数组,返回其长度,无则返回0
解题思路:可变窗口,右指针累加和,满足条件后不断收缩左窗口,寻找最短区间
cpp
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0, sum = 0, minLen = INT_MAX;
for(int right = 0; right < nums.size(); right++){
sum += nums[right];
// 满足条件,尝试收缩窗口找更短区间
while(sum >= target){
minLen = min(minLen, right - left + 1);
sum -= nums[left];
left++;
}
}
return minLen == INT_MAX ? 0 : minLen;
}
int main(){
vector<int> nums = {2,3,1,2,4,3};
cout << minSubArrayLen(7, nums) << endl; // 输出2
return 0;
}
代码详解:
-
sum累加右指针元素,当和≥目标值时,窗口合法; -
核心逻辑:合法后持续收缩左窗口,不断刷新最短长度;
-
收缩过程中实时减去左边界值,保证sum实时更新;
-
最终判断是否找到合法窗口,无则返回0,有则返回最短长度。
案例3:至多两个不同字符的最长子串(中等进阶)
题目描述:给定字符串,找出最多包含2个不同字符的最长子串长度
解题思路:哈希表统计窗口字符种类,种类超2则收缩左窗口,动态维护合法区间
cpp
#include<iostream>
#include<string>
#include<unordered_map>
#include<algorithm>
using namespace std;
int lengthOfTwoCharSub(string s) {
unordered_map<char, int> mp; // 统计字符出现次数
int left = 0, maxLen = 0;
for(int right = 0; right < s.size(); right++){
mp[s[right]]++; // 纳入新字符
// 字符种类超过2,收缩窗口
while(mp.size() > 2){
mp[s[left]]--;
if(mp[s[left]] == 0){
mp.erase(s[left]); // 次数为0,移除字符
}
left++;
}
maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
int main(){
string s = "eceba";
cout << lengthOfTwoCharSub(s) << endl; // 输出3
return 0;
}
代码详解:
-
哈希表
mp记录窗口内每个字符的出现频次,mp.size()代表字符种类数; -
右指针纳入新字符后,若种类超2,循环剔除左边界字符;
-
若左边界字符频次归零,从哈希表中删除,保证种类统计准确;
-
窗口合法后更新最大长度,实现动态伸缩求解。
案例4:最小覆盖子串(困难压轴、面试高频)
题目描述:给字符串s、t,找出s中包含t所有字符的最小子串,无则返回空串
解题思路:双哈希表统计字符匹配情况,右指针探索全覆盖窗口,左指针收缩找最小区间
cpp
#include<iostream>
#include<string>
#include<unordered_map>
#include<climits>
using namespace std;
string minWindow(string s, string t) {
unordered_map<char, int> need, window;
// 统计t中需要的字符及数量
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 cur = s[right];
if(need.count(cur)){
window[cur]++;
// 当前字符数量达标,有效匹配数+1
if(window[cur] == need[cur]) valid++;
}
// 所有字符匹配,窗口全覆盖,尝试收缩
while(valid == need.size()){
// 更新最小窗口
if(right - left + 1 < minLen){
minLen = right - left + 1;
start = left;
}
// 收缩左边界
char leftCur = s[left];
if(need.count(leftCur)){
if(window[leftCur] == need[leftCur]) valid--;
window[leftCur]--;
}
left++;
}
}
return minLen == INT_MAX ? "" : s.substr(start, minLen);
}
int main(){
string s = "ADOBECODEBANC", t = "ABC";
cout << minWindow(s, t) << endl; // 输出BANC
return 0;
}
代码详解:
-
need存储目标字符串t的字符需求,window存储当前窗口字符; -
valid统计完全匹配的字符种类,等于t的字符种类时,窗口全覆盖; -
全覆盖后收缩左窗口,不断刷新最小窗口长度和起始位置;
-
收缩时若某个字符匹配失效,减少有效匹配数,终止收缩;
-
最终根据记录的起始位置和最小长度,截取结果子串。
四、上下篇核心总结(思维升华)
-
上篇固定窗口:窗口大小恒定,先成型、再滑动,适合定长区间统计、最值查找,逻辑简单、容错率高;
-
下篇可变窗口:窗口动态伸缩,先扩张、再收缩,适合最优区间求解(最长/最短),适配绝大多数算法面试题;
-
通用思维:所有滑动窗口都是「右指针探索,左指针修正」,全程单次遍历,摒弃暴力双层循环,是区间问题的最优解思路。
谢谢