【算法进阶】滑动窗口与前缀和:从“和为 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. 窗口状态维护:
  • 如何快速知道当前窗口是否满足条件?
  • 是查 HashSet 的 size?还是对比数组?或者是维护一个 valid 计数器?
  1. 单调队列的应用场景:
  • 当你不仅需要维护窗口的状态,还需要维护窗口内的最值(最大/最小)时,单调队列是标准解法。

额外拓展:双端队列 vs 堆

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


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

相关推荐
个 人 练 习 生2 分钟前
C++中的内存管理
开发语言·c++·经验分享·学习
databook3 分钟前
手把手带你走一遍:机器学习模型如何用FastAPI和Docker部署
python·docker·fastapi
续写烂尾3 分钟前
MK SD NAND(贴片式TF卡)替代Nor Flash:1Gb容量与256Mb Nor
算法·microsoft
启雀AI5 分钟前
培训管理系统的 AI 智能陪练完整功能逻辑,以家电门店销售为例的剧本框架
人工智能·ai·软件需求·培训系统·培训平台
hunteritself8 分钟前
夯爆了!Qoder 狂肝 2 小时,293 个测试全绿,Credits 一分没扣
前端·人工智能·chrome·深度学习·机器学习
RPAdaren11 分钟前
AI 舆情智能体频繁断跑、漏抓发酵?90% 团队都踩了同一层坑
大数据·人工智能
Jet450512 分钟前
第5篇 一条线做深,一条线做宽,研究网络才长得出来
人工智能
wflynn12 分钟前
Claude 自主发现类 CRISPR 结构的新型逆转录酶系统:Anthropic 生命科学实验室的早期成果
人工智能·ai
浅安的邂逅15 分钟前
260926-OpenAI 急停最强模型训练:智能体借 DNS 漏洞联网,还泄露了 GitHub token
人工智能·chatgpt·开源·openai·ai日报
DanCheng-studio20 分钟前
计算机技术与科学毕业设计简单的方向建议
python·毕业设计·毕设