跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

滑动窗口实战:串联所有单词的子串与最小覆盖子串

滑动窗口算法在处理字符串匹配问题中非常高效。本文通过串联所有单词的子串和最小覆盖子串两道经典题目,展示了如何利用哈希表配合滑动窗口解决变长或定长区间匹配问题。重点讲解了将单词视为字符的步长控制技巧,以及使用数组优化字符频次统计的方法,帮助读者掌握此类问题的通用解法。

星云发布于 2026/3/28更新于 2026/8/2234 浏览
滑动窗口实战:串联所有单词的子串与最小覆盖子串

滑动窗口实战:两道经典字符串匹配题

在算法面试中,滑动窗口是处理连续子串或子数组问题的利器。今天我们来深入探讨两个高频考点:串联所有单词的子串和最小覆盖子串。这两道题虽然场景不同,但核心思想都是通过动态调整窗口边界来寻找最优解。

1. 串联所有单词的子串

这道题要求找到包含 words 列表中所有单词(每个单词长度相同)的起始位置。乍一看像是复杂的排列组合,但如果我们把每个单词看作一个'超级字符',问题就简化成了寻找异位词的问题。

核心思路

关键在于理解窗口的移动方式:

  • 既然每个单词长度固定为 len,那么窗口的左右指针每次移动的步长也应该是 len。
  • 我们需要遍历整个字符串,但因为是从不同偏移量开始的,所以外层循环需要执行 len 次,分别以 0 到 len-1 作为起始偏移。
  • 内部维护一个哈希表统计当前窗口内单词的出现次数,并与目标单词表的频次进行比对。
代码实现
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> ret;
        unordered_map<string, int> hash1; // 统计目标单词频次
        
        for(auto& e : words) {
            hash1[e]++;
        }
        
        int len = words[0].size(); // 单词长度
        int m = words.size();      // 单词数量
        int n = s.size();          // 字符串总长度
        
        // 从 0 到 len-1 开始遍历,覆盖所有可能的起始偏移
        for(int i = 0; i < len; i++) {
            unordered_map<string, int> hash2; // 当前窗口单词频次
            int count = 0;                    // 有效单词计数
            int left = i;
            
            // 注意:right 每次增加 len
            for(int right = i; right + len <= n; right += len) {
                string str1 = s.substr(right, len);
                hash2[str1]++;
                
                // 如果当前单词在目标中存在且未超标,计入有效数
                if(hash2[str1] <= hash1[str1]) {
                    count++;
                }
                
                // 窗口大小超过所需范围,收缩左侧
                if(right - left + 1 > len * m) {
                    string str2 = s.substr(left, len);
                    if(hash2[str2] <= hash1[str2]) {
                        count--;
                    }
                    hash2[str2]--;
                    left += len;
                }
                
                // 如果有效单词数等于目标总数,记录结果
                if(count == m) {
                    ret.push_back(left);
                }
            }
        }
        return ret;
    }
};
细节解析

这里有一个容易踩坑的地方:s.size() 返回的是 size_t(无符号类型),而 len 是 int。直接相减可能导致负数被解释为极大值。所以在循环条件中,我使用了 right + len <= n 这种形式,并提前将 s.size() 转为 int 赋值给 n,避免类型转换带来的潜在越界风险。

算法流程 算法流程

2. 最小覆盖子串

如果说上一题是'定长单词'的滑动窗口,这一题则是典型的'变长区间'问题。我们需要在字符串 s 中找到包含 t 所有字符的最短子串。

核心思路

使用双指针维护一个滑动窗口 [left, right]:

  1. 右指针扩张:不断向右移动 right,将字符加入窗口,直到窗口包含了 t 的所有字符。
  2. 左指针收缩:一旦满足条件,尝试移动 left 缩小窗口,同时保持满足条件,以更新最小长度。
  3. 频次统计:用哈希表(或数组)记录 t 中字符的需求量和窗口中实际拥有的数量。
代码实现

为了提升效率,针对 ASCII 字符集,我们可以直接用长度为 128 的数组替代哈希表。

class Solution {
public:
    string minWindow(string s, string t) {
        int hash1[128] = {0}; // 记录 t 中字符需求
        int kinds = 0;        // 记录 t 中不同字符的种类数
        
        for(char c : t) {
            if(hash1[c]++ == 0) {
                kinds++;
            }
        }
        
        int left = 0, right = 0;
        int count = 0;        // 窗口中已满足需求的字符种类数
        int min_len = s.size() + 1;
        int begin = -1;
        int hash2[128] = {0}; // 记录窗口中字符实际数量
        
        for(right = 0; right < s.size(); right++) {
            char c = s[right];
            hash2[c]++;
            
            // 当窗口中某字符数量达到需求时,有效种类数加一
            if(hash2[c] == hash1[c]) {
                count++;
            }
            
            // 当所有字符都满足需求时,尝试收缩左边界
            while(count == kinds) {
                if(min_len > right - left + 1) {
                    min_len = right - left + 1;
                    begin = left;
                }
                
                // 移除左边界字符
                if(hash2[s[left]] == hash1[s[left]]) {
                    count--;
                }
                hash2[s[left]]--;
                left++;
            }
        }
        
        return begin != -1 ? s.substr(begin, min_len) : "";
    }
};
关键点总结
  • 判断条件:不要每次循环都遍历哈希表比较,而是维护一个 count 变量。只有当某个字符的数量刚好从'不足'变为'满足'时,count 才增加;反之亦然。这样可以将时间复杂度优化到 O(N)。
  • 数组优化:由于题目通常涉及 ASCII 字符,使用 int[128] 比 unordered_map 更快且更省内存。
  • 边界处理:最后记得检查 begin 是否更新过,如果没有找到匹配子串,应返回空字符串。

算法流程 算法流程

这两道题是滑动窗口变种的典型代表。掌握它们的关键在于理解'窗口何时扩张、何时收缩'以及'如何高效判断窗口状态'。希望这些实战经验能帮你应对类似的字符串匹配挑战。

目录

  1. 滑动窗口实战:两道经典字符串匹配题
  2. 1. 串联所有单词的子串
  3. 核心思路
  4. 代码实现
  5. 细节解析
  6. 2. 最小覆盖子串
  7. 核心思路
  8. 代码实现
  9. 关键点总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 动手学大模型应用开发:从零构建个人知识库助手
  • Python 爬虫入门基础教程:流程、模块与框架详解
  • 2023 年全国职业院校技能大赛网络建设与运维赛项样题解析
  • Go Web 开发核心理论:HTTP、数据库与模板
  • Qdrant 向量数据库完全指南:从入门到 Spring AI/LangChain4J 集成实践
  • STL map/multimap 深度剖析:接口使用与核心特性详解
  • 利用 DeepSeek 辅助开发贪吃蛇游戏
  • Kafka 核心架构与分布式存储深度解析
  • 高校论文AIGC检测标准汇总及各校要求解读
  • 33 岁前端开发者转行方向与职业规划建议
  • RAG 效果优化的关键策略与工程实践详解
  • 快速排序与归并排序的非递归实现
  • VSCode Cline 插件 Git 锁文件冲突解决方案
  • 昇腾平台 DeepSeek-R1 与 Qwen2.5 强化学习训练优化实践
  • 从猎豹到机器人:脊柱仿生学如何重塑四足机器人的运动极限
  • AMD Nitro-E 304M 轻量级扩散模型技术解析
  • Mac 系统安装与配置 Python 3.x 环境指南
  • 自然语言处理在医疗领域的应用与实战
  • OpenClaw 框架 30+ 真实场景应用拆解
  • Xcode 7 中手动链接 .dylib 库的两种方案

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online