滑动窗口是处理连续子数组/子串问题的利器,但并非所有“子数组”问题都适合用双指针滑动。有些问题(如固定和、最值、覆盖)需要结合前缀和、单调队列或哈希频次匹配等技巧。
1. 和为 K 的子数组
题目描述
给定一个整数数组 nums 和一个整数 k,请你找出该数组中和为 k 的连续子数组的个数。
解题思路
❗注意:本题不能用滑动窗口!因为数组中可能包含负数,窗口扩大/缩小无法保证单调性(即
sum不一定随right增大而增大)。
核心技巧:前缀和 + 哈希表
- 定义前缀和
sum = nums[0] + ... + nums[i] - 若存在某个
j < i,使得sum_i - sum_j = k,则子数组[j+1, i]的和为k - 等价于:查找是否存在前缀和为
sum - k - 用哈希表记录每个前缀和出现的次数,边遍历边统计
💡 关键洞察:将“子数组和”问题转化为“两个前缀和之差”,用哈希表实现 O(1) 查找。
代码实现
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var subarraySum = function(nums, k) {
const map = new Map();
map.set(0, 1); // 初始化:前缀和为0出现1次(空数组)
let sum = 0;
let count = 0;
for (let num of nums) {
sum += num;
// 查找是否存在前缀和为 (sum - k)
if (map.has(sum - k)) {
count += map.get(sum - k);
}
// 更新当前前缀和的出现次数
map.set(sum, (map.get(sum) || 0) + 1);
}
return count;
};
复杂度分析
- 时间复杂度:O(n),一次遍历
- 空间复杂度:O(n),哈希表最多存 n 个前缀和
2. 滑动窗口最大值
题目描述
给定一个整数数组 nums 和一个整数 k,表示滑动窗口的大小。请你返回每个窗口中的最大值组成的数组。
解题思路
❗注意:本题窗口固定长度,但要求动态维护窗口内的最大值,普通滑动窗口无法高效更新最值。
核心技巧:单调双端队列(Monotonic Deque)
- 使用一个双端队列
queue存储数组下标 - 队列维护单调递减:队首始终是当前窗口最大值的下标
- 每次
right右移:- 弹出队尾所有小于等于
nums[right]的元素 - 将
right入队 - 若队首下标已滑出窗口(
< left),则弹出队首 - 当窗口形成(
left >= 0),记录nums[queue[0]]
- 弹出队尾所有小于等于
💡 单调队列在 O(1) 均摊时间内维护滑动窗口最值,每个元素最多入队出队一次。
代码实现
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function (nums, k) {
const ans = [];
const queue = []; // 存储下标,维护单调递减
for (let right = 0; right < nums.length; right++) {
// 维护单调性:弹出队尾所有 <= nums[right] 的元素
while (queue.length > 0 && nums[queue[queue.length - 1]] <= nums[right]) {
queue.pop();
}
queue.push(right);
const left = right - k + 1;
// 移除已滑出窗口的队首
if (queue[0] < left) {
queue.shift();
}
// 窗口形成后记录最大值
if (left >= 0) {
ans.push(nums[queue[0]]);
}
}
return ans;
};
复杂度分析
- 时间复杂度:O(n),每个元素最多入队出队一次
- 空间复杂度:O(k),队列最多存 k 个元素
3. 最小覆盖子串
题目描述
给定字符串 s 和 t,返回 s 中包含 t 所有字符的最短子串。若不存在,返回空字符串。
解题思路
核心技巧:可变滑动窗口 + 哈希频次匹配 + 有效字符计数
- 用
need哈希表记录t中每个字符的需求频次 - 用
window哈希表记录当前窗口中各字符的实际频次 - 用
valid记录已满足需求的字符种类数(不是总个数!) right扩张窗口,加入字符;当valid === need.size时,尝试收缩left- 收缩过程中不断更新最小覆盖子串
💡
valid的设计避免了每次遍历整个哈希表判断是否覆盖,实现 O(1) 条件检查。
代码实现
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
if (t.length > s.length) return "";
const need = new Map();
for (let c of t) {
need.set(c, (need.get(c) || 0) + 1);
}
let left = 0;
let valid = 0; // 满足 need 要求的字符种类数
let start = 0;
let minLen = Infinity;
const window = new Map();
for (let right = 0; right < s.length; right++) {
const c = s[right];
// 扩大窗口
if (need.has(c)) {
window.set(c, (window.get(c) || 0) + 1);
if (window.get(c) === need.get(c)) {
valid++; // 该字符刚好满足需求
}
}
// 尝试收缩窗口:当所有字符都满足时
while (valid === need.size) {
// 更新答案
if (right - left + 1 < minLen) {
start = left;
minLen = right - left + 1;
}
const d = s[left];
left++;
if (need.has(d)) {
if (window.get(d) === need.get(d)) {
valid--; // 破坏满足状态
}
window.set(d, window.get(d) - 1);
}
}
}
return minLen === Infinity ? "" : s.slice(start, start + minLen);
};
复杂度分析
- 时间复杂度:O(n + m),
n = s.length,m = t.length,每个字符最多进出窗口一次 - 空间复杂度:O(k),k 为字符集大小(如 ASCII 为 128)
总结对比
| 题目 | 窗口类型 | 核心数据结构 | 关键技巧 |
|---|---|---|---|
| 和为 K 的子数组 | 无窗口 | 哈希表(前缀和) | 前缀和差值 → 转化为查找问题 |
| 滑动窗口最大值 | 固定窗口 | 单调双端队列 | 维护窗口内最值,O(1) 查询 |
| 最小覆盖子串 | 可变窗口 | 哈希表(频次+valid) | 频次匹配 + 有效字符计数 |
希望这篇解析对你有帮助!如果你喜欢这类结构清晰、对比鲜明的算法总结,欢迎关注我的 LeetCode 热题 100 系列 👋