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

【算法进阶】滑动窗口与前缀和:从"和为 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)

题目背景

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

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

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

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

思考流程模型

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

额外拓展:双端队列 vs 堆

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


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

相关推荐
宵时待雨2 小时前
数据结构(初阶)笔记归纳2:顺序表的实现
c语言·数据结构·笔记·算法
木木木一2 小时前
Rust学习记录--C10 泛型,Trait,生命周期
python·学习·rust
予枫的编程笔记2 小时前
【注册技巧】stackoverflow无法注册解决方案
人工智能·stackoverflow·注册技巧
WangYaolove13142 小时前
基于深度学习的身份证识别考勤系统(源码+文档)
python·mysql·django·毕业设计·源码
嘿嘿潶黑黑2 小时前
Qt中的Q_PROPERTY宏
开发语言·qt
不穿格子的程序员2 小时前
从零开始刷算法——二叉树篇:层序遍历 + 有序数组转二叉搜索树
算法
一个帅气昵称啊2 小时前
C# 14 中的新增功能
开发语言·c#
qwerasda1238522 小时前
【深度学习】如何使用YOLO11-RevCol模型进行伤口类型识别与分类 擦伤、瘀伤、烧伤、切割伤以及正常状态检测_2
人工智能·深度学习·分类
阿蒙Amon2 小时前
C#每日面试题-简述C#构造函数和析构函数
java·开发语言·c#