【算法进阶】滑动窗口与前缀和:从"和为 K"到"最小覆盖子串"的极限挑战

我的主页: 寻星探路
个人专栏: 《JAVA(SE)----如此简单!!! 》 《从青铜到王者,就差这讲数据结构!!!》
《数据库那些事!!!》 《JavaEE 初阶启程记:跟我走不踩坑》
《JavaEE 进阶:从架构到落地实战 》 《测试开发漫谈》
《测开视角・力扣算法通关》 《从 0 到 1 刷力扣:算法 + 代码双提升》
《Python 全栈测试开发之路》
没有人天生就会编程,但我生来倔强!!!
寻星探路的个人简介:


1. 题目一:和为 K 的子数组 (LeetCode 560)
题目背景
这道题虽然被归类在子串/子数组中,但它是**"滑动窗口"思想的变体------前缀和 + 哈希表**。当子数组包含负数时,标准的滑动窗口无法工作,必须借助前缀和。

核心技巧:前缀和思想
- 定义为范围内的元素之和。
- 任意子数组的和可以表示为 。
- 我们需要寻找满足的个数,等价于 。
java
public class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0; // 记录符合条件的子数组数量
int pre = 0; // 累加前缀和
// HashMap 存储 (前缀和, 该前缀和出现的次数)
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, 1); // 初始化:前缀和为 0 出现了 1 次(处理从索引 0 开始的子数组)
for (int num : nums) {
pre += num; // 计算当前前缀和
// 如果存在 pre - k,说明从那个位置到当前位置的子数组和正好为 k
if (map.containsKey(pre - k)) {
count += map.get(pre - k); // 累加次数
}
// 将当前前缀和存入或更新 map
map.put(pre, map.getOrDefault(pre, 0) + 1);
}
return count;
}
}
2. 题目二:滑动窗口最大值 (LeetCode 239)
题目背景
这是固定窗口 的极致优化版。普通遍历是 ,通过单调队列可以优化到 。

核心技巧:单调双端队列 (Deque)
- 队列性质:队列中存储元素的索引,且对应的值从大到小排列。
- 入队逻辑:新元素进入前,将队列后端所有比它小的元素全部弹出(因为它们永远不可能成为后续窗口的最大值了)。
- 出队逻辑:当窗口滑动时,检查队列头部的索引是否已经超出了窗口左边界,若是则移除。
java
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
// 单调双端队列,存放下标
Deque<Integer> deque = new LinkedList<Integer>();
// 初始化第一个窗口
for (int i = 0; i < k; ++i) {
while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
deque.pollLast(); // 保持队列单调递减
}
deque.offerLast(i);
}
int[] ans = new int[n - k + 1];
ans[0] = nums[deque.peekFirst()]; // 队头永远是当前窗口最大值
for (int i = k; i < n; ++i) {
// 入:新元素入队逻辑
while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
deque.pollLast();
}
deque.offerLast(i);
// 出:移除离开窗口的过期下标
while (deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 记录当前窗口结果
ans[i - k + 1] = nums[deque.peekFirst()];
}
return ans;
}
}
3. 题目三:最小覆盖子串 (LeetCode 76)
题目背景
这是变长滑动窗口最经典、也是最难的模板题。

核心技巧:双指针+计数器
- **右指针
right**:负责寻找"可行解",不断扩大窗口直到覆盖t中所有字符。 - **左指针
left**:负责寻找"最优解",在保持覆盖的情况下,不断收缩窗口以获得最短长度。 - **有效计数
valid**:记录当前窗口内有多少种字符达到了t要求的频次。
java
class Solution {
public String minWindow(String s, String t) {
int[] ori = new int[128]; // 记录 t 中字符频次
int[] cnt = new int[128]; // 记录当前窗口字符频次
int valid = 0, needMatch = 0; // 匹配成功的种类数 & 需要匹配的总种类数
for (char c : t.toCharArray()) {
if (ori[c] == 0) needMatch++;
ori[c]++;
}
int left = 0, right = 0, minLen = Integer.MAX_VALUE, start = -1;
while (right < s.length()) {
char cur = s.charAt(right++); // 扩大窗口
if (ori[cur] > 0) {
cnt[cur]++;
if (cnt[cur] == ori[cur]) valid++; // 某种字符数量达标
}
// 当窗口满足条件,开始收缩寻找最优解
while (valid == needMatch) {
if (right - left < minLen) {
minLen = right - left;
start = left;
}
char d = s.charAt(left++); // 缩小窗口
if (ori[d] > 0) {
if (cnt[d] == ori[d]) valid--;
cnt[d]--;
}
}
}
return start == -1 ? "" : s.substring(start, start + minLen);
}
}
4. 深度思考:滑动窗口类题目的通用心法
这类题目通常具有单调性:即窗口扩大时,某个属性(如和、字符种类)单调递增;窗口缩小时,该属性单调递减。
思考流程模型
- 属性判断:
- 窗口固定吗? 固定则用
i-k逻辑(239题)。 - 能用简单双指针吗? 如果数组里有负数,简单的
left++可能让和变大也可能变小,此时单调性破坏,需改用前缀和(560题)。
- 窗口状态维护:
- 如何快速知道当前窗口是否满足条件?
- 是查
HashSet的size?还是对比数组?或者是维护一个valid计数器?
- 单调队列的应用场景:
- 当你不仅需要维护窗口的状态,还需要维护窗口内的最值(最大/最小)时,单调队列是标准解法。
额外拓展:双端队列 vs 堆
在"滑动窗口最大值"中,我们也可以使用大顶堆(PriorityQueue)。但堆的删除复杂度是 ,总复杂度 。而单调队列通过舍弃"永远没机会当最大值"的元素,实现了 ,这是算法逻辑上的降维打击。
希望这篇总结能帮你在刷题之路上更进一步!如果你对"单调栈"或者"二分查找"结合滑动窗口的题目感兴趣,可以随时告诉我。