1. 不定长滑动窗口算法概述
滑动窗口算法是解决数组/字符串子区间问题的一把利器,尤其适合处理"连续子数组"类问题。不定长滑动窗口作为其中重要分支,其核心思想是通过动态调整窗口边界来寻找满足特定条件的最优解。
1.1 基本概念与适用场景
不定长滑动窗口与定长滑动窗口的主要区别在于窗口尺寸会随着条件变化而动态调整。它通常用于解决以下三类问题:
- 求最长子数组:在满足特定条件下寻找最长的连续子序列(如无重复字符的最长子串)
- 求最短子数组:寻找满足条件的最短连续子序列(如和≥target的最短子数组)
- 求子数组个数:统计满足特定条件的子数组数量
这类问题的共同特征是:当窗口扩大时,可能违反条件;窗口缩小时,可能满足条件。这种"单调性"使得滑动窗口算法成为最优解。
1.2 算法框架与核心要素
不定长滑动窗口的标准框架包含三个关键操作:
- 右指针移动(入队):扩展窗口,纳入新元素
- 左指针移动(出队):收缩窗口,排除元素
- 条件检查与结果更新:在每次窗口调整后验证条件并记录最优解
cpp复制int slidingWindow(vector<int>& nums) {
int left = 0, ans = 0;
for(int right = 0; right < nums.size(); right++) {
// 1. 将nums[right]加入窗口
// 2. while(窗口不符合条件) 移动left缩小窗口
// 3. 更新答案
}
return ans;
}
2. 最长子数组问题解析
2.1 无重复字符的最长子串
这是滑动窗口的经典应用,要求找到不含重复字符的最长连续子串。
解题思路:
- 使用哈希表记录字符最后出现的位置
- 当遇到重复字符时,快速跳转左指针
- 始终保持窗口内字符唯一
cpp复制class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> lastPos;
int ans = 0, left = 0;
for(int right = 0; right < s.size(); right++) {
if(lastPos.count(s[right])) {
left = max(left, lastPos[s[right]] + 1);
}
lastPos[s[right]] = right;
ans = max(ans, right - left + 1);
}
return ans;
}
};
复杂度分析:
- 时间复杂度:O(n),每个字符最多被访问两次
- 空间复杂度:O(min(m,n)),m为字符集大小
关键点:当发现重复字符时,left直接跳到该字符上次出现位置的下一位,避免不必要的逐步移动。
2.2 最多K次重复的最长子串
这是上一题的变种,允许每个字符最多出现k次。
cpp复制class Solution {
public:
int maxKRepeatingSubstring(string s, int k) {
unordered_map<char, int> cnt;
int ans = 0, left = 0;
for(int right = 0; right < s.size(); right++) {
cnt[s[right]]++;
while(cnt[s[right]] > k) {
cnt[s[left++]]--;
}
ans = max(ans, right - left + 1);
}
return ans;
}
};
变种思考:如果将问题改为"每个字符最多出现k次的最长子串",只需将条件改为cnt[s[right]] > k即可。这种灵活性体现了滑动窗口的通用性。
3. 带约束条件的最长子数组
3.1 删除一个元素后全1子数组
这个问题要求找到删除一个元素后,全为1的最长子数组。
问题转化:等价于寻找最多包含一个0的最长子数组,长度减一即为答案。
cpp复制class Solution {
public:
int longestSubarray(vector<int>& nums) {
int ans = 0, left = 0, zeroCount = 0;
for(int right = 0; right < nums.size(); right++) {
if(nums[right] == 0) zeroCount++;
while(zeroCount > 1) {
if(nums[left++] == 0) zeroCount--;
}
ans = max(ans, right - left); // 注意不减1,因为要删除一个元素
}
return ans;
}
};
注意事项:
- 窗口内0的个数不超过1
- 最终结果要减1是因为必须删除一个元素
- 全1数组的特殊情况需要单独处理
3.2 水果成篮问题
这个问题可以抽象为:找到最多包含两种元素的最长子数组。
cpp复制class Solution {
public:
int totalFruit(vector<int>& fruits) {
unordered_map<int, int> basket;
int ans = 0, left = 0;
for(int right = 0; right < fruits.size(); right++) {
basket[fruits[right]]++;
while(basket.size() > 2) {
if(--basket[fruits[left]] == 0) {
basket.erase(fruits[left]);
}
left++;
}
ans = max(ans, right - left + 1);
}
return ans;
}
};
优化技巧:使用哈希表记录元素最后出现位置,可以优化左指针移动效率:
cpp复制int totalFruit(vector<int>& fruits) {
unordered_map<int, int> lastPos;
int ans = 0, left = 0;
for(int right = 0; right < fruits.size(); right++) {
lastPos[fruits[right]] = right;
if(lastPos.size() > 2) {
auto it = min_element(lastPos.begin(), lastPos.end(),
[](auto& a, auto& b) { return a.second < b.second; });
left = it->second + 1;
lastPos.erase(it);
}
ans = max(ans, right - left + 1);
}
return ans;
}
4. 成本约束类问题
4.1 预算内最大字符串转换
这个问题要求在预算限制内,找到可以转换的最大长度子串。
cpp复制class Solution {
public:
int equalSubstring(string s, string t, int maxCost) {
int ans = 0, left = 0, cost = 0;
for(int right = 0; right < s.size(); right++) {
cost += abs(s[right] - t[right]);
while(cost > maxCost) {
cost -= abs(s[left] - t[left]);
left++;
}
ans = max(ans, right - left + 1);
}
return ans;
}
};
性能优化:可以预处理成本数组,避免重复计算:
cpp复制int equalSubstring(string s, string t, int maxCost) {
vector<int> costs(s.size());
for(int i = 0; i < s.size(); i++) {
costs[i] = abs(s[i] - t[i]);
}
int ans = 0, left = 0, sum = 0;
for(int right = 0; right < costs.size(); right++) {
sum += costs[right];
while(sum > maxCost) {
sum -= costs[left++];
}
ans = max(ans, right - left + 1);
}
return ans;
}
5. 排序与滑动窗口结合
5.1 平衡数组的最小移除量
这个问题需要先排序,然后使用滑动窗口寻找最大平衡子数组。
cpp复制class Solution {
public:
int minRemoval(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
int maxSave = 0, left = 0;
for(int right = 0; right < nums.size(); right++) {
while((long)nums[left] * k < nums[right]) {
left++;
}
maxSave = max(maxSave, right - left + 1);
}
return nums.size() - maxSave;
}
};
算法分析:
- 排序时间复杂度:O(nlogn)
- 滑动窗口时间复杂度:O(n)
- 总时间复杂度:O(nlogn)
- 空间复杂度:O(1)(不考虑排序的栈空间)
关键点:排序后问题转化为寻找满足nums[left]*k >= nums[right]的最大窗口,这种"先排序后滑动窗口"的思路在解决极差类问题时非常有效。
6. 滑动窗口的变种与技巧
6.1 多指针滑动窗口
某些问题可能需要维护多个指针来跟踪不同条件。例如,寻找包含所有指定字符的最短子串:
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, 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);
}
};
6.2 滑动窗口与前缀和结合
对于涉及子数组和的问题,可以结合前缀和与滑动窗口:
cpp复制class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0, sum = 0, ans = INT_MAX;
for(int right = 0; right < nums.size(); right++) {
sum += nums[right];
while(sum >= target) {
ans = min(ans, right - left + 1);
sum -= nums[left++];
}
}
return ans == INT_MAX ? 0 : ans;
}
};
7. 常见问题与调试技巧
7.1 滑动窗口的常见错误
- 指针移动条件错误:确保左右指针移动的条件与问题要求严格一致
- 结果更新时机不当:结果应在每次窗口调整后更新,但要注意更新条件
- 边界条件处理不足:空输入、全符合/全不符合等情况需要单独考虑
- 哈希表清理不彻底:当元素计数为0时应从哈希表中移除
7.2 调试技巧
- 打印窗口状态:在循环中打印左右指针和窗口内容
cpp复制cout << "Window [" << left << "," << right << "]: ";
for(int i = left; i <= right; i++) cout << s[i] << " ";
cout << endl;
- 可视化跟踪:对于复杂问题,可以画出指针移动示意图
- 小规模测试:先用简单测试用例验证基本逻辑
- 极端情况测试:空串、全相同字符、最大规模输入等
7.3 性能优化建议
- 哈希表优化:当字符集有限时,可以用数组代替哈希表
cpp复制int count[128] = {0}; // ASCII字符集
- 提前终止:当找到可能的最大解时可以提前结束
cpp复制if(ans == s.size()) break; // 已经找到最大可能解
- 减少冗余计算:预处理数据或缓存中间结果
8. 滑动窗口问题分类训练
为了熟练掌握不定长滑动窗口,建议按以下类别进行专项训练:
8.1 基础训练
- [x] 无重复字符的最长子串(LeetCode 3)
- [x] 最大连续1的个数 III(LeetCode 1004)
- [x] 长度最小的子数组(LeetCode 209)
8.2 进阶挑战
- [ ] 最小覆盖子串(LeetCode 76)
- [ ] 字符串的排列(LeetCode 567)
- [ ] 找到字符串中所有字母异位词(LeetCode 438)
8.3 综合应用
- [ ] K个不同整数的子数组(LeetCode 992)
- [ ] 替换后的最长重复字符(LeetCode 424)
- [ ] 最大连续1的个数 II(LeetCode 487)
在实际面试中,滑动窗口问题通常会与其他概念结合考察。建议在掌握基本模式后,重点练习如何将实际问题转化为滑动窗口模型的能力。