目录
[239. 滑动窗口最大值](#239. 滑动窗口最大值)
[76. 最小覆盖子串(Hard)](#76. 最小覆盖子串(Hard))
这两道题是算法面试必考的滑动窗口体系核心题 :一道是单调队列 的经典应用(滑动窗口最大值),一道是滑动窗口 + 哈希表的 Hard 级天花板(最小覆盖子串)。我用最通俗的思路拆解 + 满分注释代码,新手也能直接看懂、上手就写!
239. 滑动窗口最大值
题目描述
给定一个数组 nums 和一个滑动窗口大小 k,从数组最左侧移动到最右侧,每次窗口移动一位,返回每个窗口内的最大值。
解题思路
暴力解法会遍历所有窗口,时间复杂度 O(n∗k) 直接超时。最优解:自定义单调递减队列 **核心思想:
- 维护一个双端队列 ,保证队列从队头到队尾单调递减;
- 队首永远是当前窗口的最大值;
- 入队:新元素入队时,移除所有比它小的队尾元素(小元素不可能成为最大值,直接舍弃);
- 出队:窗口滑动时,若要移除的元素恰好是队首最大值,才将其出队;
- 每个窗口直接取队首元素,就是当前窗口最大值。
满分代码(自定义单调队列)
java
运行
/**
* 自定义单调递减队列
* 核心保证:队首元素永远是当前窗口的最大值
*/
class MyDeque {
Deque<Integer> deque = new LinkedList<>();
// 出队操作:只有要移除的元素是队首最大值时,才弹出
void poll(int val) {
if (!deque.isEmpty() && val == deque.peek()) {
deque.poll();
}
}
// 入队操作:移除所有比当前元素小的尾部元素,保证队列单调递减
void add(int val) {
while (!deque.isEmpty() && val > deque.getLast()) {
deque.removeLast();
}
deque.add(val);
}
// 获取当前窗口最大值(队首元素)
int peek() {
return deque.peek();
}
}
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
// 结果数组长度:n - k + 1
int[] res = new int[n - k + 1];
int index = 0;
MyDeque myDeque = new MyDeque();
// 1. 先初始化第一个窗口的元素
for (int i = 0; i < k; i++) {
myDeque.add(nums[i]);
}
// 记录第一个窗口的最大值
res[index++] = myDeque.peek();
// 2. 滑动窗口遍历后续元素
for (int i = k; i < n; i++) {
// 窗口右移,移除窗口最左侧的元素
myDeque.poll(nums[i - k]);
// 加入窗口新元素
myDeque.add(nums[i]);
// 记录当前窗口最大值
res[index++] = myDeque.peek();
}
return res;
}
}
核心亮点
- 时间复杂度:O(n)(每个元素仅入队、出队一次)
- 空间复杂度:O(k)(队列最多存储 k 个元素)
- 面试必背:单调队列专门解决滑动窗口极值问题
76. 最小覆盖子串(Hard)
题目描述
给你一个字符串 s、一个字符串 t。返回 s 中涵盖 t 所有字符的最小子串;如果不存在,返回空字符串。
解题思路
这是滑动窗口的天花板题目 ,核心套路:双指针 + 双哈希表 + 精准计数
- 用两个哈希表:
need记录目标字符串 t 的字符需求,window记录当前窗口内的字符; - 右指针不断右移扩大窗口,直到窗口包含 t 所有字符;
- 左指针开始收缩窗口,尽可能缩小长度,同时更新最小子串;
- 用
strnumber记录窗口内完全匹配的字符种类数,精准控制窗口收缩时机。
满分代码(极简注释版)
java
运行
class Solution {
public String minWindow(String s, String t) {
// 边界特判
if (s == null || t == null || s.isEmpty() || t.isEmpty()) {
return "";
}
// need:记录目标字符串t的字符及数量
Map<Character, Integer> need = new HashMap<>();
// window:记录当前滑动窗口内的字符及数量
Map<Character, Integer> window = new HashMap<>();
// 滑动窗口左右指针
int left = 0, right = 0;
// 窗口内已匹配的字符种类数(和need完全匹配的数量)
int matchCount = 0;
// 记录最小子串的长度和起始索引
int minLen = Integer.MAX_VALUE;
int start = 0;
// 初始化need哈希表
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
// 右指针扩大窗口
while (right < s.length()) {
char rightChar = s.charAt(right);
right++;
// 更新窗口内字符计数
if (need.containsKey(rightChar)) {
window.put(rightChar, window.getOrDefault(rightChar, 0) + 1);
// 当前字符数量完全匹配,匹配数+1
if (window.get(rightChar).equals(need.get(rightChar))) {
matchCount++;
}
}
// 窗口已包含所有t的字符,开始收缩左指针,优化最小长度
while (matchCount == need.size()) {
// 更新最小子串
if (right - left < minLen) {
start = left;
minLen = right - left;
}
// 左指针右移,缩小窗口
char leftChar = s.charAt(left);
if (window.containsKey(leftChar)) {
window.put(leftChar, window.get(leftChar) - 1);
// 字符数量不匹配,匹配数-1
if (window.get(leftChar) < need.get(leftChar)) {
matchCount--;
}
}
left++;
}
}
// 没有找到合法子串返回空,否则返回最小子串
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
}
核心关键点
- 双哈希表解耦:严格区分目标字符和窗口字符,避免计数混乱;
- matchCount 精准控制 :只有字符数量完全相等时才计数,是收缩窗口的唯一依据;
- 收缩窗口优化:找到合法窗口后,尽可能缩小,保证得到最小子串。
刷题总结
- 滑动窗口最大值 :单调队列 专属场景,解决滑动窗口极值问题,队首恒为最值;
- 最小覆盖子串 :滑动窗口 + 哈希表经典 Hard 题,掌握「扩大窗口→匹配→收缩窗口」模板,同类题一通百通;