子串专题部分

子串专题

Hot100 子串专题共 3 题:和为 K 的子数组、滑动窗口最大值、最小覆盖子串。这三题难度跨度较大,从中等到困难,涉及前缀和、单调队列、滑动窗口三种不同的核心技巧。


一、和为 K 的子数组(#560)

题意

给一个整数数组 nums 和整数 k,返回数组中和为 k 的连续子数组的个数。

复制代码
输入:nums = [1, 1, 1], k = 2
输出:2

输入:nums = [1, 2, 3], k = 3
输出:2  (子数组 [1,2] 和 [3])

注意:数组元素可以是负数,所以滑动窗口不适用(窗口收缩的前提是元素非负时和单调)。

前置知识:前缀和

前缀和 prefix[i]prefix[i]prefix[i] 表示 nums[0]+nums[1]+⋯+nums[i−1]nums[0] + nums[1] + \cdots + nums[i-1]nums[0]+nums[1]+⋯+nums[i−1],定义 prefix[0]=0prefix[0] = 0prefix[0]=0。

子数组 [i,j][i, j][i,j] 的和等于 prefix[j+1]−prefix[i]prefix[j+1] - prefix[i]prefix[j+1]−prefix[i]。

复制代码
nums   =  [1,  2,  3]
prefix = [0,  1,  3,  6]

子数组[0,1]的和 = prefix[2] - prefix[0] = 3 - 0 = 3 ✓
子数组[1,2]的和 = prefix[3] - prefix[1] = 6 - 1 = 5 ✓

思路

问题转化:找有多少对 (i,j)(i, j)(i,j) 满足 prefix[j]−prefix[i]=kprefix[j] - prefix[i] = kprefix[j]−prefix[i]=k,即 prefix[i]=prefix[j]−kprefix[i] = prefix[j] - kprefix[i]=prefix[j]−k。

遍历到位置 jjj 时,查询之前有多少个前缀和等于 prefix[j]−kprefix[j] - kprefix[j]−k,累加到答案里。用 unordered_map 记录每个前缀和出现的次数,边遍历边查询,O(1)O(1)O(1) 完成每次查询。

复制代码
nums = [1, 1, 1], k = 2
初始:mp = {0: 1}(前缀和为0出现1次,对应空前缀)

遍历:
  prefix=1: 查 1-2=-1,mp里没有 → ans+=0,mp={0:1, 1:1}
  prefix=2: 查 2-2=0,mp里有1次 → ans+=1,mp={0:1, 1:1, 2:1}
  prefix=3: 查 3-2=1,mp里有1次 → ans+=1,mp={0:1, 1:1, 2:1, 3:1}

ans = 2 ✓

为什么初始化 mp[0] = 1?因为如果某个前缀和 prefix[j]prefix[j]prefix[j] 本身就等于 kkk,那么 prefix[j]−k=0prefix[j] - k = 0prefix[j]−k=0,对应的是从下标 0 开始的子数组,需要被统计进来,所以空前缀(前缀和为 0)要提前存入。

代码

cpp 复制代码
class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> mp;
        mp[0] = 1; // 空前缀,前缀和为 0
        int prefix = 0, ans = 0;
        for (int x : nums) {
            prefix += x;
            // 查询之前有多少前缀和等于 prefix - k
            if (mp.count(prefix - k)) {
                ans += mp[prefix - k];
            }
            mp[prefix]++;
        }
        return ans;
    }
};

复杂度

  • 时间 :O(n)O(n)O(n),单次遍历,每次哈希操作 O(1)O(1)O(1)
  • 空间 :O(n)O(n)O(n),哈希表最多存 n+1n+1n+1 个前缀和

二、滑动窗口最大值(#239)

题意

给一个整数数组 nums 和窗口大小 k,窗口从左到右滑动,每次只能看到窗口内的 kkk 个元素,返回每个窗口位置的最大值。

复制代码
输入: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

前置知识:单调队列

单调队列是一个两端都能操作的队列(双端队列 deque),队列内的元素保持单调性。

这道题需要的是单调递减队列:队头是当前窗口的最大值,从队头到队尾单调递减。

维护规则:

  • 新元素从队尾入队前,把队尾所有比它小的元素弹出(它们永远不可能成为后续窗口的最大值)

  • 如果队头元素的下标已经滑出窗口,从队头弹出

    队列里存的是下标,方便判断是否滑出窗口

思路

遍历 nums,对每个位置 right

  1. 清理队尾 :把队尾所有满足 nums[队尾] <= nums[right] 的元素弹出,再把 right 入队

  2. 清理队头 :如果队头下标 <= right - k,说明已滑出窗口,弹出队头

  3. 记录答案 :当 right >= k-1 时(窗口已满),队头就是当前窗口最大值

    nums = [1,3,-1,-3,5,3,6,7], k=3

    right=0(1): 队列[0]
    right=1(3): 3>1,弹出0,队列[1]
    right=2(-1): -1<3,直接入队,队列[1,2],窗口满,最大值=nums[1]=3
    right=3(-3): -3<-1,直接入队,队列[1,2,3]
    队头1未滑出(1 <= 3-3=0? 否),最大值=nums[1]=3
    right=4(5): 5>-3弹3,5>-1弹2,5>3弹1,队列[4]
    最大值=nums[4]=5
    right=5(3): 3<5,直接入队,队列[4,5]
    最大值=nums[4]=5
    right=6(6): 6>3弹5,6<5? 不,6>5弹4,队列[6]
    最大值=nums[6]=6
    right=7(7): 7>6弹6,队列[7]
    最大值=nums[7]=7

代码

cpp 复制代码
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        deque<int> dq; // 存下标,单调递减(队头最大)
        vector<int> res;
        for (int right = 0; right < nums.size(); right++) {
            // 清理队尾:保持单调递减
            while (!dq.empty() && nums[dq.back()] <= nums[right]) {
                dq.pop_back();
            }
            dq.push_back(right);
            // 清理队头:滑出窗口的元素
            if (dq.front() <= right - k) {
                dq.pop_front();
            }
            // 窗口已满,记录答案
            if (right >= k - 1) {
                res.push_back(nums[dq.front()]);
            }
        }
        return res;
    }
};

复杂度

  • 时间 :O(n)O(n)O(n),每个元素最多入队一次、出队一次
  • 空间 :O(k)O(k)O(k),单调队列最多存 kkk 个元素

三、最小覆盖子串(#76)

题意

给字符串 st,找 s 中包含 t 所有字符(含重复)的最短子串,不存在则返回空字符串。

复制代码
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"

输入:s = "a", t = "aa"
输出:""  (s 里只有一个 a,无法覆盖 t 里的两个 a)

思路

这是滑动窗口的经典困难题,窗口可变长度,目标是找满足条件的最短窗口。

窗口条件 :窗口内包含 t 的所有字符,且每个字符的出现次数都不少于 t 中的次数。

用两个哈希表 needwindow

  • needt 中每个字符需要的数量
  • window:当前窗口内每个字符的数量

用变量 valid 记录窗口内已经满足数量要求的字符种数,当 valid == need.size() 时,窗口覆盖了 t

流程

  1. right 向右扩张,把字符纳入窗口,如果某字符的窗口数量恰好达到需要量,valid++

  2. valid == need.size() 时,窗口已覆盖 t,记录当前窗口长度,然后收缩 left

  3. 收缩时,如果移出的字符导致某字符数量低于需要量,valid--,退出收缩循环

  4. 重复直到 right 到达末尾

    s = "ADOBECODEBANC", t = "ABC"
    need = {A:1, B:1, C:1}

    right扩张到覆盖ABC:
    [ADOBEC] → valid=3,覆盖了,记录长度6,开始收缩left
    移出A → A的窗口数量0 < need[A]=1 → valid=2,停止收缩

    right继续扩张:
    [DOBECODEBA] → 又找到A,valid=3
    收缩left直到窗口最短:[CODEBA] → 长度6
    移出C → valid=2

    right继续:
    [CODEBANC] → 找到C,valid=3
    收缩:移出C → valid=2,停止 → 此时 [ODEBANC] 不对
    实际上收缩到 [BANC],长度4,更新最短

    最终答案:[BANC]

代码

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char, int> need, window;
        for (char c : t) need[c]++;

        int left = 0, valid = 0;
        int start = 0, minLen = INT_MAX; // 记录最短窗口的起点和长度

        for (int right = 0; right < s.size(); right++) {
            char c = s[right];
            // 纳入右边字符
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c]) valid++; // 该字符恰好满足
            }
            // 窗口已覆盖 t,收缩左边
            while (valid == need.size()) {
                // 更新最短窗口
                if (right - left + 1 < minLen) {
                    minLen = right - left + 1;
                    start = left;
                }
                char d = s[left];
                left++;
                if (need.count(d)) {
                    if (window[d] == need[d]) valid--; // 移出前恰好满足,移出后不满足
                    window[d]--;
                }
            }
        }
        return minLen == INT_MAX ? "" : s.substr(start, minLen);
    }
};

注意收缩时先判断 valid--window[d]-- 的顺序:因为判断条件是 window[d] == need[d](移出这个字符会导致不满足),所以要在减之前判断。

复杂度

  • 时间 :O(n+m)O(n + m)O(n+m),nnn 为 s 的长度,mmm 为 t 的长度。leftright 各最多移动 nnn 次,初始化 need 需要 O(m)O(m)O(m)
  • 空间 :O(∣Σ∣)O(|\Sigma|)O(∣Σ∣),两个哈希表的 key 都是字符,最多 O(1)O(1)O(1)

四、专题小结

题目 核心技巧 关键数据结构 时间 空间
和为 K 的子数组 前缀和 + 哈希查询 unordered_map O(n)O(n)O(n) O(n)O(n)O(n)
滑动窗口最大值 单调队列维护区间最大值 deque O(n)O(n)O(n) O(k)O(k)O(k)
最小覆盖子串 可变滑动窗口 + 双哈希表 unordered_map O(n+m)O(n+m)O(n+m) O(1)O(1)O(1)

三题用了三种完全不同的技巧,但有一个共同点:都在避免重复计算 。前缀和把区间求和变成两个前缀相减,单调队列把区间最大值查询变成 O(1)O(1)O(1),滑动窗口通过移动左右指针复用了窗口状态。

相关推荐
H_BB1 小时前
第17届蓝桥杯备战历程
c++·算法·职场和发展·蓝桥杯
anew___2 小时前
算法分析与设计课程全算法核心概述|期末复习+知识梳理
算法
daad7772 小时前
记录一次上下文切换次数的统计
服务器·c++·算法
fliter2 小时前
Cloudflare 推出 Flagship:为 AI 时代重新设计的功能开关服务
后端·算法
生成论实验室2 小时前
《源·觉·知·行·事·物:生成论视域下的统一认知语法》第十七章 科学与人心的重聚
人工智能·算法·架构·知识图谱·创业创新
chao1898442 小时前
局部保局投影(LPP)算法实现
算法
ShoreKiten2 小时前
cpp考前急救
数据结构·c++·算法
纪伊路上盛名在2 小时前
机器学习中常见的距离度量函数 Distance metrics
人工智能·算法·机器学习·数据分析·统计
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2026.05.07 题目:3660. 找到所有可以到达的最大值
笔记·算法·leetcode