单调队列力扣题(leetcode)

单调队列力扣题(leetcode)

76. 最小覆盖子串

难度:中等

相关标签:哈希表字符串滑动窗口

题目:

给定两个字符串 st,长度分别是 mn,返回 s 中的 最短窗口 子串,使得该子串包含 t 中的每一个字符(包括重复字符)。如果没有这样的子串,返回空字符串 ""

测试用例保证答案唯一。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

示例 2:

输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。

示例 3:

输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

提示:

  • \(m == s.length\)
  • \(n == t.length\)
  • \(1 <= m, n <= 10^5\)
  • \(s 和 t由英文字母组成\)

进阶:你能设计一个在 O(m + n) 时间内解决此问题的算法吗?

分析

题目是这样的:给定两个字符串 S 和 T,我们要在 S 中找到最小的窗口,使得这个窗口包含 T 中的所有字符。

举个例子,如果 S 是 "ADOBECODEBANC" 这样一串共 13 个字符,T 是 "ABC",那么答案就是 "BANC",因为它是包含 A、B 和 C 的最小子串。如果找不到这样的窗口,就返回空字符串。

暴力解法会枚举 S 的所有子串,然后检查每个子串是否包含 T 的所有字符,这样的时间复杂度是 O (N³),无法通过面试。

最优的解法是使用滑动窗口加哈希表。核心思路是这样的:我们用两个哈希表,一个记录 T 中每个字符需要的数量,另一个记录当前窗口中每个字符的数量。然后用两个指针,左指针和右指针来维护这个窗口。右指针不断向右扩展窗口,直到窗口包含 T 的所有字符,这时窗口就是有效的。然后左指针开始收缩窗口,在保持窗口有效的前提下,尽可能缩小窗口的大小。每当找到一个有效窗口,我们就更新记录的最小窗口。

具体怎么操作。还是这个例子,S 是 "ADOBECODEBANC",T 是 "ABC"。开始之前先说明几个变量:need 表记录每个目标字符需要多少个,这里 A、B、C 都需要一个;window 表记录当前窗口里 A、B、C 各有多少个;valid 表示有多少种字符已经在窗口里数量达标了,当 valid 等于 3 时,窗口才有效。同时用 start 和 length 记录目前找到的最短答案。

初始时,left 和 right 都在最左边,window 为空,valid 等于 0,length 先设为无穷大。

第一轮扩展,right 读到 A,把 A 加入窗口,A 的数量变成 1,A 达标,所以 valid 变成 1。继续读到 D、O,他们不是目标字符,不影响。读到 B,把 B 加入窗口,B 的数量变成 1,B 达标,所以 valid 变成 2。读到 E,不影响。读到 C,把 C 加入窗口,C 的数量变成 1,C 达标,所以 valid 变成 3,窗口第一次有效。窗口一有效就尝试更新答案,当前窗口是 "ADOBEC",长度 6,比之前更短,所以 start 记在当前 left,length 更新为 6。

接下来开始第一轮收缩,left 指向 A,把它移出窗口,A 的数量从 1 变成 0,A 不达标,valid 从 3 变成 2,窗口失效,停止收缩。

继续第二轮扩展,right 依次读到 O、D、E,都不影响。再读到 B,把 B 加入窗口,B 的数量从 1 变成 2,超过需求不算额外达标,所以 valid 还是 2。再读到 A,把 A 加入窗口,A 的数量从 0 变成 1,A 达标,valid 回到 3,窗口再次有效。这次窗口更长,所以不更新最小长度。

开始第二轮收缩,left 依次移出 D、O,不影响。移出 B,B 的数量从 2 变成 1,仍达标,valid 还是 3。移出 E,不影响。此时窗口仍然有效,长度是 6,并不比之前记录的长度小,所以不更新最小长度。继续移除 C,C 的数量从 1 变成 0,不达标,valid 从 3 变成 2,窗口失效,停止收缩。

开始下一轮扩展,right 读到 N,不影响。读到 C,把 C 加入窗口,C 的数量从 0 变成 1,C 达标,valid 回到 3,窗口再次有效。开始收缩,left 依次移除 O、D、E,期间窗口仍有效,但长度更短了,所以相应更新 start 和最小长度,现在窗口正好是 "BANC",长度是 4。继续收缩,再移出 B,B 的数量从 1 变成 0,不达标,valid 从 3 变成 2,窗口失效。但此时 right 已经到头,无法再扩展,所以最终答案就是 "BANC",长度为 4。

代码:

题目大意

给定字符串st,在s中找到包含t所有字符的最小长度子串,保证答案唯一(无解返回空串)。

解题思路(滑动窗口 + 哈希表)

核心:枚举终点i,找到最近的起点j,使[j, i]包含t所有字符,维护最小长度。

  1. 准备工作:
    • ht哈希表统计t中各字符的出现次数;
    • hs哈希表统计窗口[j, i]中各字符的出现次数;
    • count记录窗口中 “有效字符数”(即不超过t中计数的字符数)。
  2. 滑动窗口步骤:
    • 扩展窗口:i右移,更新hs[nums[i]],若hs[c] ,count++`(有效字符增加);
    • 收缩窗口:当count == t.size()(窗口有效),尝试右移j:若hs[s[j]] > ht[s[j]](字符多余),则hs[s[j]]--j++
    • 更新答案:每次收缩后,用i-j+1更新最小子串长度。
class Solution 
{
public:
    // 函数功能:在s中找到包含t所有字符的最短子串
    string minWindow(string s, string t) 
    {
        // hs:记录当前滑动窗口内 每个字符的出现次数
        // ht:记录目标字符串t 每个字符需要的次数(固定不变)
        unordered_map<char, int> hs, ht;
        
        // 先把t里所有字符统计好,存入ht
        for (auto c: t) ht[c] ++ ;

        // res:存储最终答案(最短子串)
        string res;
        // cnt:记录当前窗口内【有效字符】的总个数(满足t需求的字符)
        int cnt = 0;
        
        // 滑动窗口双指针:i右指针,j左指针
        for (int i = 0, j = 0; i < s.size(); i ++ )
        {
            // 1. 右指针i字符进入窗口,计数+1
            hs[s[i]] ++ ;
            
            // 2. 如果这个字符是t需要的,且数量没超过需求 → 有效字符cnt+1
            if (hs[s[i]] <= ht[s[i]]) cnt ++ ;

            // 3. 尝试收缩左指针j:
            // 如果左指针字符数量 > t需要的数量 → 可以删掉,j右移
            while (hs[s[j]] > ht[s[j]]) hs[s[j ++ ]] -- ;
            
            // 4. 如果当前窗口有效(包含了t所有字符)
            if (cnt == t.size())
            {
                // 5. 如果答案为空 或 当前窗口更短 → 更新最短子串
                if (res.empty() || i - j + 1 < res.size())
                    res = s.substr(j, i - j + 1);
            }
        }
        // 返回最终最短子串
        return res;
    }
};

相似题目

串联所有单词的子串困难

长度最小的子数组中等

滑动窗口最大值困难

字符串的排列中等

最小区间困难

最小窗口子序列困难

统计重新排列后包含另一个字符串的子字符串数目 II困难

统计重新排列后包含另一个字符串的子字符串数目 I中等

239. 滑动窗口最大值

难度:困难

相关标签:队列数组滑动窗口单调队列堆(优先队列)

题目:

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值


[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

示例 2:

输入:nums = [1], k = 1
输出:[1]

提示:

  • \(1 <= nums.length <= 10^5\)
  • \(-10^4 <= nums[i] <= 10^4\)
  • \(1 <= k <= nums.length\)

代码:

class Solution 
{
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) 
    {
        vector<int> res;          // 存储最终结果
        deque<int> q;             // 双端队列,存储nums的下标(核心!)
        int n = nums.size();
        
        for (int i = 0; i < n; i++)
        {
            // 步骤1:移除队头——超出窗口范围的下标(窗口左边界是 i-k+1)
            if (!q.empty() && q.front() < i - k + 1)  q.pop_front();
            
            // 步骤2:维护队列单调性——保证队头是当前窗口最大值
            // 移除队尾所有 <= 当前元素的下标(这些元素不可能成为后续窗口的最大值)
            while (!q.empty() && nums[q.back()] <= nums[i])  q.pop_back();
            
            // 步骤3:当前下标入队
            q.push_back(i);
            
            // 步骤4:窗口形成(i >= k-1),记录当前窗口最大值(队头)
            if (i >= k - 1)  res.push_back(nums[q.front()]);
        }
        
        return res;
    }
};

相似题目

最小覆盖子串困难

最小栈中等

至多包含两个不同字符的最长子串中等

粉刷房子 II困难

跳跃游戏 VI中等

预算内的最多机器人数目困难

礼盒的最大甜蜜度中等

执行 K 次操作后的最大分数中等

438. 找到字符串中所有字母异位词

难度:中等

相关标签:哈希表字符串滑动窗口

题目:

给定两个字符串 sp,找到 s 中所有 p异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

示例 1:

输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

示例 2:

输入:l1 = [], l2 = []
输出:[]

示例 3:

输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

提示:

  • \(1 <= s.length, p.length <= 3 * 10^4\)
  • \(s 和 p 仅包含小写字母\)

代码:

class Solution 
{
public:
    vector<int> findAnagrams(string s, string p) 
    {
        int m = s.size(), n = p.size();
        if(m < n) 
        // 哈希表:统计目标字符串p中每个字符的需要次数
        unordered_map<char, int> cnt;
        // 初始化:把p的所有字符存入哈希表,记录每个字符的出现次数
        for (auto c: p) cnt[c] ++ ;
        
        vector<int> res;          // 存储最终结果(所有异位词的起始索引)
        int tot = cnt.size();     // tot:p中「不同字符的总数」(比如p=abc则tot=3)
        
        // 滑动窗口双指针:
        // i:右指针(窗口右边界)
        // j:左指针(窗口左边界)
        // satisfy:当前窗口中「满足p字符数量要求的不同字符数」(比如a的数量刚好等于p中a的数量,就算1个)
        for (int i = 0, j = 0, satisfy = 0; i < s.size(); i ++ ) 
        {
            // 步骤1:右指针i的字符进入窗口,对应计数-1
            // 关键逻辑:如果减完后等于0 → 说明这个字符的数量刚好满足p的要求 → satisfy+1
            if ( -- cnt[s[i]] == 0) satisfy ++ ;
            
            // 步骤2:窗口长度超过p的长度 → 收缩左指针j(保证窗口长度等于p.size())
            while (i - j + 1 > p.size()) 
            {
                // 收缩前检查:如果当前j位置的字符计数是0 → 说明移出后该字符不再满足要求 → satisfy-1
                if (cnt[s[j]] == 0) satisfy -- ;
                // j位置字符移出窗口,对应计数+1,j右移
                cnt[s[j ++ ]] ++ ;
            }
            
            // 步骤3:如果满足要求的字符数等于p的不同字符总数 → 说明当前窗口是异位词
            // 记录窗口起始索引j
            if (satisfy == tot) res.push_back(j);
        }
        
        return res;
    }
};
class Solution 
{
public:
    vector<int> findAnagrams(string s, string p) 
    {
        int m = s.size(), n = p.size();
        // 边界条件:s长度小于p,不可能有异位词,直接返回空结果
        if(m < n) return {};
        
        // cnt:哈希表存储「字符的供需差值」= p中字符需要量 - 窗口中字符实际量
        // 初始时窗口为空,所以cnt[c] = p中c的需要量(实际量为0)
        unordered_map<char, int> cnt;
        // 初始化cnt:统计p中每个字符的需要量,作为初始供需差值
        for (auto c: p) cnt[c] ++ ;
        
        vector<int> res;          // 存储最终结果(所有异位词的起始索引)
        int tot = cnt.size();     // tot:p中「不同字符的总数」(比如p=abc则tot=3)
        
        // 滑动窗口双指针:
        // i:右指针(窗口右边界),负责将字符加入窗口
        // j:左指针(窗口左边界),负责将字符移出窗口(控制窗口长度)
        // satisfy:当前窗口中「供需差值=0」的字符种类数(即刚好满足p需求的字符数)
        for (int i = 0, j = 0, satisfy = 0; i < s.size(); i ++ ) 
        {
            // 步骤1:字符s[i]进入窗口 → 消耗需求,供需差值-1
            // 关键逻辑:减完后供需差值=0 → 该字符从「缺/多余」变为「刚好满足」→ satisfy+1
            if ( -- cnt[s[i]] == 0) satisfy ++ ;
            
            // 步骤2:窗口长度超过p的长度 → 收缩左指针j(保证窗口长度严格等于p.size())
            while (i - j + 1 > p.size()) 
            {
                // 收缩前检查:如果当前j位置字符的供需差值=0 → 移出后该字符从「刚好满足」变「缺」→ satisfy-1
                if (cnt[s[j]] == 0) satisfy -- ;
                // 步骤:字符s[j]移出窗口 → 归还需求,供需差值+1;同时j右移(窗口左边界收缩)
                // 注:j++是后置自增,先取j的当前值操作,再将j+1
                cnt[s[j ++ ]] ++ ;
            }
            
            // 步骤3:如果所有p的不同字符都达到「供需平衡」→ 当前窗口是p的异位词
            // 记录窗口起始索引j(此时窗口长度=n,j是窗口第一个字符的下标)
            if (satisfy == tot) res.push_back(j);
        }
        
        return res;
    }
};

代码分析

一、cnt 的准确定义(最核心,先记死)

cnt哈希表 / 数组(仅小写字母时用数组更高效),存储的是:

目标字符串 p 中「字符 c 的需要量」 - 当前滑动窗口中「字符 c 的实际量」

(简称:字符 c 的「供需差值」)

三种核心状态(用 p=abc 举例,初始 cnt [a]=1、cnt [b]=1、cnt [c]=1)

cnt [c] 的值 状态含义 通俗解释
> 0(如 1) 供 < 需 窗口中 c 的数量 < p 需要的数量,缺 cnt [c] 个,不满足
= 0 供 = 需 窗口中 c 的数量 = p 需要的数量,刚好满足
< 0(如 - 1) 供 > 需 窗口中 c 的数量 > p 需要的数量,多 -cnt [c] 个,满足但有多余

补充:不在 p 中的字符

如果字符 c 不在 p 中,p 对 c 的需要量为 0 → cnt [c] 初始值为 0(哈希表默认 / 数组初始化),此时:

  • cnt [c] = 0 - 窗口中 c 的实际量 → 窗口加入 c 则 cnt [c]=-1,移出则 cnt [c] 回 0。

二、cnt 的核心处理逻辑(「进窗口」和「出窗口」的逆操作)

滑动窗口的本质是「动态维护窗口内字符的供需差值」,所有操作都是围绕 cnt 展开的,核心是 「进则减,出则加」

1. 字符进入窗口(右指针 i 移动,s [i] 加入窗口)
// 步骤1:消耗需求 → 供需差值-1
--cnt[s[i]];
// 步骤2:如果减完后供需平衡 → 该字符从「不满足/多余」变为「刚好满足」→ 满足的字符种类数+1
if (cnt[s[i]] == 0) satisfy++;
举例(p=abc,cnt [a] 初始 = 1):
  • 窗口加入第一个 a → cnt [a] = 1-1 = 0 → satisfy+1(a 刚好满足);
  • 窗口加入第二个 a → cnt [a] = 0-1 = -1 → cnt≠0 → satisfy 不变(a 满足但多余)。
2. 字符移出窗口(左指针 j 移动,s [j] 移出窗口)
// 步骤1:先判断——移出前该字符是否「刚好满足」
if (cnt[s[j]] == 0) satisfy--; // 移出后从「满足」变「不满足」→ 满足种类数-1
// 步骤2:归还需求 → 供需差值+1,同时j右移
cnt[s[j++]]++;
举例(p=abc,窗口中有 2 个 a,cnt [a]=-1):
  • 移出第一个 a → cnt [a]=-1≠0 → satisfy 不变;cnt [a]++→0(a 从多余变刚好满足);
  • 再移出第二个 a → cnt [a]=0 → satisfy-1;cnt [a]++→1(a 从满足变缺 1 个)。

三、完整处理流程(结合题目逻辑)

以「找到字符串中所有字母异位词」为例,完整流程如下:

步骤 0:初始化
unordered_map<char, int> cnt;
for (auto c : p) cnt[c]++; // 初始化cnt:存储p中每个字符的需要量(供需差值=需要量-0)
int tot = cnt.size();      // p中「不同字符的总数」(比如p=abc则tot=3)
int satisfy = 0;           // 当前窗口中「刚好满足」的字符种类数
vector<int> res;           // 存储异位词起始索引
步骤 1:滑动窗口遍历 s
for (int i = 0, j = 0; i < s.size(); i++) 
{
    // 1. 右指针:字符进窗口,更新cnt和satisfy
    --cnt[s[i]];
    if (cnt[s[i]] == 0) satisfy++;

    // 2. 左指针:窗口长度超p的长度时,收缩窗口(保证窗口长度=len(p))
    while (i - j + 1 > p.size()) 
    {
        // 2.1 移出前判断:是否刚好满足
        if (cnt[s[j]] == 0) satisfy--;
        // 2.2 字符出窗口,更新cnt,j右移
        cnt[s[j++]]++;
    }

    // 3. 判断:所有字符都刚好满足 → 当前窗口是异位词
    if (satisfy == tot) res.push_back(j);
}

四、关键逻辑验证(两种典型场景)

场景 1:s [j] 在 p 中且 cnt [s [j]]=0(刚好满足)
  • p=abc,s [j]=a,cnt [a]=0(窗口中 a 刚好 1 个);
  • 移出前:cnt [a]=0 → satisfy--(从 3→2);
  • 移出后:cnt [a]++→1(窗口中 a=0,缺 1 个),j 右移;
  • 逻辑:a 从「满足」变「不满足」,satisfy 同步减少,符合预期。
场景 2:s [j] 不在 p 中(如 s [j]=d)
  • p=abc,cnt [d] 初始 = 0;窗口加入 d 后 cnt [d]=-1;
  • 移出时:cnt [d]=-1≠0 → satisfy 不变;
  • 移出后:cnt [d]++→0(恢复初始状态),j 右移;
  • 逻辑:d 从未被计入 satisfy,移出时不影响,符合预期。

总结(核心记忆点)

  1. cnt 的本质:供需差值 = p 的需要量 - 窗口的实际量,能表达「缺、够、多」三种状态;
  2. 核心操作:字符进窗口→cnt-1(消耗需求),字符出窗口→cnt+1(归还需求);
  3. satisfy 的作用:仅统计「供需差值 = 0」的字符种类数,等于 tot 时窗口是异位词;
  4. 边界兼容:不在 p 中的字符全程不影响 satisfy,无需额外判断。

记住这四句话,整个滑动窗口处理逻辑就完全通了,无论字符是否在 p 中、cnt 是正 / 负 / 零,都能精准判断!

相似题目

有效的字母异位词简单

字符串的排列中等

posted @ 2026-04-04 17:59  CodeMagicianT  阅读(3)  评论(0)    收藏  举报