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

请添加图片描述

我的主页:寻星探路
没有人天生就会编程,但我生来倔强!!!

寻星探路的个人简介:

请添加图片描述
请添加图片描述

1. 题目一:和为 K 的子数组 (LeetCode 560)

题目背景

  这道题虽然被归类在子串/子数组中,但它是**“滑动窗口”思想的变体——前缀和 + 哈希表**。当子数组包含负数时,标准的滑动窗口无法工作,必须借助前缀和。

在这里插入图片描述

核心技巧:前缀和思想

  • 定义为范围内的元素之和。
  • 任意子数组的和可以表示为 。
  • 我们需要寻找满足的个数,等价于 。
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)

  • 队列性质:队列中存储元素的索引,且对应的值从大到小排列。
  • 入队逻辑:新元素进入前,将队列后端所有比它小的元素全部弹出(因为它们永远不可能成为后续窗口的最大值了)。
  • 出队逻辑:当窗口滑动时,检查队列头部的索引是否已经超出了窗口左边界,若是则移除。
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)

题目背景

  这是变长滑动窗口最经典、也是最难的模板题。

在这里插入图片描述

核心技巧:双指针+计数器

  1. **右指针 right**:负责寻找“可行解”,不断扩大窗口直到覆盖 t 中所有字符。
  2. **左指针 left**:负责寻找“最优解”,在保持覆盖的情况下,不断收缩窗口以获得最短长度。
  3. **有效计数 valid**:记录当前窗口内有多少种字符达到了 t 要求的频次。
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. 深度思考:滑动窗口类题目的通用心法

  这类题目通常具有单调性:即窗口扩大时,某个属性(如和、字符种类)单调递增;窗口缩小时,该属性单调递减。

思考流程模型

  1. 属性判断
  • 窗口固定吗? 固定则用 i-k 逻辑(239题)。
  • 能用简单双指针吗? 如果数组里有负数,简单的 left++ 可能让和变大也可能变小,此时单调性破坏,需改用前缀和(560题)。
  1. 窗口状态维护
  • 如何快速知道当前窗口是否满足条件?
  • 是查 HashSetsize?还是对比数组?或者是维护一个 valid 计数器?
  1. 单调队列的应用场景
  • 当你不仅需要维护窗口的状态,还需要维护窗口内的最值(最大/最小)时,单调队列是标准解法。

额外拓展:双端队列 vs 堆

  在“滑动窗口最大值”中,我们也可以使用大顶堆(PriorityQueue)。但堆的删除复杂度是 ,总复杂度 。而单调队列通过舍弃“永远没机会当最大值”的元素,实现了 ,这是算法逻辑上的降维打击。


希望这篇总结能帮你在刷题之路上更进一步!如果你对“单调栈”或者“二分查找”结合滑动窗口的题目感兴趣,可以随时告诉我。