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

滑动窗口不仅能解决基础的子串/子数组问题,还能处理字符统计、多模式匹配等复杂场景。本文继续介绍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字符)。
相关推荐
旧梦吟2 小时前
脚本网页 linux内核源码讲解
linux·前端·stm32·算法·html5
2301_789015623 小时前
C++:二叉搜索树
c语言·开发语言·数据结构·c++·算法·排序算法
leiming610 小时前
C++ vector容器
开发语言·c++·算法
Xの哲學11 小时前
Linux流量控制: 内核队列的深度剖析
linux·服务器·算法·架构·边缘计算
yaoh.wang12 小时前
力扣(LeetCode) 88: 合并两个有序数组 - 解法思路
python·程序人生·算法·leetcode·面试·职场和发展·双指针
LYFlied13 小时前
【每日算法】 LeetCode 56. 合并区间
前端·算法·leetcode·面试·职场和发展
艾醒13 小时前
大模型原理剖析——多头潜在注意力 (MLA) 详解
算法
艾醒13 小时前
大模型原理剖析——DeepSeek-V3深度解析:671B参数MoE大模型的技术突破与实践
算法