🌟 核心思想总结
三种双指针模式
| 模式 | 特点 | 适用场景 | 经典例题 |
|---|---|---|---|
| 同向双指针 (滑动窗口) | 左右指针同向移动,维护一个窗口 | 子数组/子串问题,满足某条件的最长/最短区间 | 无重复字符的最长子串、长度最小的子数组 |
| 对向双指针 | 左右指针从两端向中间移动 | 有序数组查找、两数之和、回文判断 | 两数之和 II、反转字符串 |
| 快慢指针 | 快慢指针以不同速度移动 | 链表环检测、中点查找、重复元素 | 环形链表、寻找链表中点 |
📚 滑动窗口解题框架
通用模板
cpp
int slidingWindow(vector<int>& nums, int k) {
int left = 0; // 左指针
int result = 0; // 结果
unordered_map<int, int> freq; // 频率统计(根据需要)
for (int right = 0; right < nums.size(); right++) {
// 1. 右指针扩张,更新状态
freq[nums[right]]++;
// 或 sum += nums[right];
// 2. 当不满足条件时,收缩左指针
while (/* 不满足条件 */) {
// 更新状态
freq[nums[left]]--;
// 或 sum -= nums[left];
left++;
}
// 3. 更新结果
result = max(result, right - left + 1);
// 或 result += right - left + 1;
}
return result;
}
🎯 问题识别技巧
什么时候用滑动窗口?
- 关键词:连续子数组、子串、最长/最短
- 限制条件:不超过K个某元素、至少包含K个某元素
- 数据特征:数组/字符串,通常需要找满足条件的区间
判断依据
cpp
// 如果是这些问题,考虑滑动窗口:
- "找到最短的连续子数组,其和 ≥ target"
- "找到最长的子串,其中最多有K个重复字符"
- "找到包含所有字符的最小子串"
- "统计满足条件的子数组个数"
🔍 关键决策点
1. 窗口何时扩张?
- 总是:右指针每次循环向右移动一步
- 将新元素纳入窗口,更新相关统计
2. 窗口何时收缩?
-
当不满足题目条件时收缩
-
关键:正确识别"不满足条件"的判断
cpp// 各种条件的判断: while (sum >= target) // 最小长度:条件达成时收缩 while (freq[c] > k) // 最多K个重复:超过时收缩 while (unique_chars > k) // 最多K种字符:超过时收缩 while (zero_count > k) // 最多翻转K个0:超过时收缩
3. 如何更新答案?
- 最长问题 :
ans = max(ans, right-left+1)在while循环后 - 最短问题 :
ans = min(ans, right-left+1)在while循环内 - 计数问题 :
ans += right-left+1在while循环后
💡 经典问题分类与解法
类型1:固定条件窗口
cpp
// 问题:满足条件的最长/最短窗口
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];
left++;
}
}
return ans == INT_MAX ? 0 : ans;
}
类型2:频率限制窗口
cpp
// 问题:最多K个重复字符/最多K种字符
int lengthOfLongestSubstringKDistinct(string s, int k) {
int left = 0, ans = 0;
unordered_map<char, int> freq;
for (int right = 0; right < s.size(); right++) {
freq[s[right]]++;
while (freq.size() > k) { // 超过K种字符
freq[s[left]]--;
if (freq[s[left]] == 0) freq.erase(s[left]);
left++;
}
ans = max(ans, right - left + 1);
}
return ans;
}
类型3:转换思维窗口
cpp
// 问题:从两端删除使和等于x → 找中间和为total-x的最长子数组
int minOperations(vector<int>& nums, int x) {
int total = accumulate(nums.begin(), nums.end(), 0);
int target = total - x; // 关键转换
int left = 0, sum = 0, max_len = -1;
for (int right = 0; right < nums.size(); right++) {
sum += nums[right];
while (sum > target && left <= right) {
sum -= nums[left];
left++;
}
if (sum == target) {
max_len = max(max_len, right - left + 1);
}
}
return max_len == -1 ? -1 : nums.size() - max_len;
}
🚀 教学要点
给初学者的建议
-
先画图理解
- 画出数组和指针移动过程
- 用具体例子手动模拟
-
从暴力法思考
暴力:O(n²) → 枚举所有子数组 优化:滑动窗口 O(n) → 利用连续性 -
记住三个核心问题
- 窗口什么时候扩张?(右指针右移)
- 窗口什么时候收缩?(不满足条件时)
- 什么时候记录答案?(收缩后/收缩时)
-
从简单模板开始
cppint left = 0; for (int right = 0; right < n; right++) { // 加入nums[right] while (/* 不满足条件 */) { // 移除nums[left] left++; } // 更新答案 }
常见错误与调试
- 死循环:确保while循环条件最终能打破
- 漏掉答案:检查答案更新位置是否正确
- 边界错误:注意数组索引越界
- 初始值错误:ans初始化为合适值
练习题进阶路径
Level 1: 固定窗口大小问题
Level 2: 条件简单的可变窗口
Level 3: 需要统计频率的窗口
Level 4: 需要转换思维的问题
Level 5: 多条件复合窗口
📝 一句话总结各类问题
- 最长无重复子串:字符频率 >1 时收缩
- 长度最小子数组:和 ≥ target 时收缩并记录
- 最大连续1的个数III:0的数量 >k 时收缩
- 乘积小于K的子数组:乘积 ≥k 时收缩,计数用窗口长度
- 水果成篮:水果种类 >2 时收缩
- 最小覆盖子串:需要额外记录匹配条件
- 替换后的最长重复字符:窗口长度-最大频率 >k 时收缩
- 字符串的排列:固定长度窗口+频率匹配
🎁 终极心法
滑动窗口 = 右指针探索 + 左指针维持合法性 + 适时记录答案
掌握这个心法,配合足够的练习,就能解决大部分滑动窗口问题。开始时多画图,多模拟,慢慢就会形成直觉。