【算法题】滑动窗口 (二)

滑动窗口不仅能解决基础的子串/子数组问题,还能处理字符统计、多模式匹配等复杂场景。本文继续介绍4道经典滑动窗口题目,覆盖"类型限制""异位词匹配""多单词串联""最小覆盖子串"等核心场景。

一、水果成篮

题目描述:

农场的果树用数组 fruits 表示(fruits[i] 是第 i 棵树的水果类型),你只有2个篮子(每个篮子只能装单一类型水果),需从某棵树开始连续采摘,返回能收集的水果最大数目。

示例

  • 输入:fruits = [1,2,1],输出:3(采摘全部3棵树)

解题思路:

用滑动窗口维护"最多包含2种水果类型的连续区间":

  1. 用哈希表 windows 统计窗口内水果类型的出现次数。
  2. 右指针 right 遍历数组,将当前水果加入窗口并更新哈希表。
  3. 若窗口内类型数超过2,移动左指针缩小窗口(同时减少对应类型的计数,若计数为0则从哈希表中删除)。
  4. 每次调整后,更新最大水果数目。

完整代码:

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        unordered_map<int, int> windows;
        int ret = 0;
        for(int left = 0, right = 0; right < fruits.size(); right++) {
            windows[fruits[right]]++;
            while(windows.size() > 2) {
                windows[fruits[left]]--;
                if(windows[fruits[left]] == 0)
                    windows.erase(fruits[left]);
                left++;
            }
            ret = max(ret, right - left + 1);
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n),每个元素最多被左右指针各遍历一次。
  • 空间复杂度: O ( 1 ) O(1) O(1),哈希表最多存储2种水果类型。

二、找到字符串中所有字母异位词

题目描述:

给定字符串 sp,找出 s 中所有 p 的异位词(字母相同但顺序不同的子串)的起始索引。

示例

  • 输入:s = "cbaebabacd", p = "abc",输出:[0,6](子串 "cba""bac"abc 的异位词)

解题思路:

固定长度的滑动窗口 (窗口长度= p 的长度),配合哈希数组统计字符频率:

  1. hash1 统计 p 中各字符的出现次数。
  2. hash2 统计窗口内 s 子串的字符频率,count 记录窗口中"符合 p 字符频率要求"的字符数。
  3. 右指针扩展窗口,左指针在窗口长度超过 p 时收缩,同时更新 hash2count
  4. count == p.size() 时,当前窗口是异位词,记录左指针索引。

完整代码:

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> ret;
        int hash1[26] = {0};
        for(auto ch : p) hash1[ch - 'a']++;

        int hash2[26] = {0};
        int n = p.size();
        for(int left = 0, right = 0, count = 0; right < s.size(); right++) {
            char in = s[right];
            if(++hash2[in - 'a'] <= hash1[in - 'a']) count++;
            if(right - left + 1 > n) {
                int out = s[left++];
                if(hash2[out - 'a']-- <= hash1[out - 'a']) count--;
            }
            if(count == n) ret.push_back(left);
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n),ns 的长度,每个字符仅被遍历一次。
  • 空间复杂度: O ( 1 ) O(1) O(1),哈希数组大小固定(26个小写字母)。

三、串联所有单词的子串

题目描述:

给定字符串 s 和单词数组 words(所有单词长度相同),返回 s 中包含 words 所有单词(任意顺序)的子串的起始索引。

示例

  • 输入:s = "barfoothefoobarman", words = ["foo","bar"],输出:[0,9](子串 "barfoo""foobar"words 的串联)

解题思路:

将"单词"视为"字符",用固定步长的滑动窗口(步长=单词长度):

  1. hash1 统计 words 中各单词的出现次数。
  2. 按单词长度 lenlen 组(避免遗漏起始位置),每组内用滑动窗口遍历:
    • 窗口长度= words.size() * len,用 hash2 统计窗口内单词的出现次数。
    • 右指针每次移动 len 步(取一个单词),左指针在窗口超限时同步移动 len 步。
    • count 记录窗口中"符合 words 单词频率要求"的单词数,当 count == words.size() 时记录左指针索引。

完整代码:

cpp 复制代码
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> ret;
        unordered_map<string, int> hash1;
        for(auto s : words) hash1[s]++;

        int len = words[0].size(), m = words.size();
        for(int i = 0; i < len; i++) {
            unordered_map<string, int> hash2;
            for(int left = i, right = i, count = 0; right + len <= s.size(); right += len) {
                string in = s.substr(right, len);
                hash2[in]++;
                if(hash1.count(in) && hash2[in] <= hash1[in]) count++;
                if(right - left + 1 > len * m) {
                    string out = s.substr(left, len);
                    if(hash1.count(out) && hash2[out] <= hash1[out]) count--;
                    hash2[out]--;
                    left += len;
                }
                if(count == m) ret.push_back(left);
            }
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度: O ( n × l e n ) O(n \times len) O(n×len),ns 的长度,len 是单词长度(分组遍历的总复杂度为 O ( n ) O(n) O(n))。
  • 空间复杂度: O ( m ) O(m) O(m),mwords 的长度(哈希表存储单词频率)。

四、最小覆盖子串

题目描述:

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

示例

  • 输入:s = "ADOBECODEBANC", t = "ABC",输出:"BANC"

解题思路:

用滑动窗口维护"包含 t 所有字符的区间",动态收缩窗口找最短长度:

  1. hash1 统计 t 中各字符的出现次数,kinds 记录 t 中不同字符的数量。
  2. hash2 统计窗口内字符的出现次数,count 记录窗口中"满足 t 字符频率要求"的字符种类数。
  3. 右指针扩展窗口,当 count == kinds 时,尝试移动左指针收缩窗口(同时更新最短子串的长度和起始位置)。
  4. 遍历结束后,根据记录的起始位置和长度返回结果。

完整代码:

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        int hash1[128] = {0};
        int kinds = 0;
        for(auto ch : t) 
            if(hash1[ch]++ == 0) kinds++;
        int hash2[128] = {0};

        int len = INT_MAX, begin = -1;
        for(int left = 0, right = 0, count = 0; right < s.size(); right++) {
            char in = s[right];
            hash2[in]++;
            if(hash2[in] == hash1[in]) count++;
            while(count == kinds) {
                if(right - left + 1 < len) {
                    len = right - left + 1;
                    begin = left;
                }
                char out = s[left++];
                if(hash2[out] == hash1[out]) count--;
                hash2[out]--;
            }
        }
        if(begin == -1) return "";
        return s.substr(begin, len);
    }
};

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n),ns 的长度,每个字符仅被遍历一次。
  • 空间复杂度: O ( 1 ) O(1) O(1),哈希数组大小固定(128个ASCII字符)。
相关推荐
AlenTech14 分钟前
198. 打家劫舍 - 力扣(LeetCode)
算法·leetcode·职场和发展
Z1Jxxx17 分钟前
0和1的个数
数据结构·c++·算法
ldccorpora17 分钟前
Chinese News Translation Text Part 1数据集介绍,官网编号LDC2005T06
数据结构·人工智能·python·算法·语音识别
重生之后端学习18 分钟前
21. 合并两个有序链表
java·算法·leetcode·链表·职场和发展
源代码•宸19 分钟前
Leetcode—1266. 访问所有点的最小时间【简单】
开发语言·后端·算法·leetcode·职场和发展·golang
YuTaoShao41 分钟前
【LeetCode 每日一题】712. 两个字符串的最小ASCII删除和——(解法一)记忆化搜索
算法·leetcode·职场和发展
知乎的哥廷根数学学派1 小时前
基于物理信息嵌入与多维度约束的深度学习地基承载力智能预测与可解释性评估算法(以模拟信号为例,Pytorch)
人工智能·pytorch·python·深度学习·算法·机器学习
古城小栈1 小时前
Rust 丰富&好用的 格式化语法
前端·算法·rust
AuroraWanderll1 小时前
类和对象(六)--友元、内部类与再次理解类和对象
c语言·数据结构·c++·算法·stl