《算法题讲解指南:优选算法-滑动窗口》--15.串联所有单词的子串,16.最小覆盖子串

《算法题讲解指南:优选算法-滑动窗口》--15.串联所有单词的子串,16.最小覆盖子串

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》

《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

15. 串联所有单词的子串

题目链接:

题目描述:

题目示例:

解法(滑动窗口+哈希表):

算法思路:

C++算法代码:

算法总结及流程解析:

16. 最小覆盖子串

题目链接:

题目描述:

题目示例:

解法 (滑动窗口+哈希表):

算法思路:

算法流程:

C++算法代码:

算法总结及流程解析:

结束语


15. 串联所有单词的子串

题目链接:

30. 串联所有单词的子串 - 力扣(LeetCode)

题目描述:

题目示例:

解法(滑动窗口+哈希表):

算法思路:
  • 如果我们把每一个单词看成一个个字母,问题就变成了找到【字符串中所有的字母的异位词】。无非就是之前处理的对象是一个个字符,我们这里处理的对象是一个一个的单词。

有以下几点不同:

  • 哈希表需要使用 hash<string,int>
  • left 和 right 移动的步长是单词的长度(len)
  • 滑动窗口执行的次数也是 len 次

C++算法代码:

class Solution { public: vector<int> findSubstring(string s, vector<string>& words) { //解法:滑动窗口 + 哈希表(容器) vector<int> ret; unordered_map<string, int> hash1; //统计 words 中所有单词出现的频次 //string表示存放的数据是string类型,int表示数据存放的次数 for(auto e : words) { hash1[e]++; } int len = words[0].size(); //len为 words 中每个字符串的长度 int m = words.size(); //m为 words 的长度,目的是为了和 count 相比较 int left = 0; int right = 0; for(int i = 0; i < len; i++) { unordered_map<string, int> hash2;//存放s中的子串 //放到第一个for循环中目的是:每轮判断完成后hash2需要进行重置 //避免上一轮hash2中存放的数据对第二轮造成影响 int count = 0;//统计窗口中“有效字符串”的个数 left = i; //for(right = i; right <= (int)s.size() - len; right += len) //写成上面s.size() - len的形式一定要注意size()返回的类型是size_t //当size_t类型与int类型进行运算时,结果会类型转换出size_t类型 //如果此时s.size() - len为负数则会变成最大值,导致substr越界访问right //所以我们需要先将s.size()返回类型强转成int类型才能进行运算 for(right = i; right + len <= s.size(); 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; } };

算法总结及流程解析:

16. 最小覆盖子串

题目链接:

76. 最小覆盖子串 - 力扣(LeetCode)

题目描述:

题目示例:

解法 (滑动窗口+哈希表):

算法思路:

      研究对象是连续的区间,因此可以尝试使用滑动窗口的思想来解决。

      如何判断当前窗口的所有字符是符合要求的呢?

  • 我们可以使用两个哈希表。其中一个将目标串的信息统计起来,另一个哈希表动态的维护窗口内字符串的信息。
  • 当动态哈希表中包含目标串中所有字符,并且对应的个数都不小于目标串的哈希表中各个字符的个数,那么当前的窗口就是一种可行的方案。
算法流程:

  1. 定义两个全局的哈希表:hash1用来记录子串的信息,hash2用来记录目标串 t 的信息;

  2. 实现一个接口函数,判断当前窗口是否满足要求:

    2.1 遍历两个哈希表中对应位置的元素

      2.1.1 如果 t 中某个字符的数量大于窗口中字符的数量,也就是 hash2 某个位置大于 hash1 。说明不匹配,返回 false。

      2.1.2 如果全部匹配,返回 true。

主函数中:

  1.先将 t 的信息放入 hash2 中;

  2.初始化一些变量:左右指针:left = 0,right = 0;目标子串的长度:len = INT_MAX;目标子串的起始位置:begin;(通过目标子串的起始位置和长度,我们就可以找到结果)

  3.当 right 小于字符串 s 的长度时,一直下列循环:

    3.1 将当前遍历到的元素扔进 hash1 中;

    3.2 检测当前窗口是否满足条件;

      3.2.1 如果满足条件:

                        判断当前窗口是否变小。如果变小:更新长度 len,以及字符串的起始位置 begin

                        判断完毕后,将左侧的元素滑出窗口,顺便更新到 hash1;

                        重复上述两个过程,直到窗口不满足条件;

    3.3 right++,遍历下一个元素;

  4. 判断 len 的长度是否等于 INT_MAX;

    4.1 如果相等,说明没有匹配,返回空串;

    4.2 如果不相等,说明匹配,返回 s 中从 retleft 位置往后 len 长度的字符串。

C++算法代码:

class Solution { public: string minWindow(string s, string t) { // //解法一:滑动窗口 + 哈希表(使用容器),count 标记“有效字符”的个数 // unordered_map<char, int> hash1; // for(auto c : t) // { // hash1[c]++; // } // int count = 0; int left = 0; int right = 0; // int n = t.size(); int min_len = s.size() + 1; int begin = -1; // string ret; // unordered_map<char, int> hash2; // for(right = 0; right < s.size(); right++) // { // hash2[s[right]]++; //进窗口 // if(hash2[s[right]] <= hash1[s[right]]) // { // count++; // } // while(hash2[s[left]] > hash1[s[left]]) //判断 // { // hash2[s[left]]--; //出窗口 // left++; // } // if(count == n) // { // if(min_len > right - left + 1) //更新结果 // { // min_len = right - left + 1; // begin = left; // } // } // } // if(begin != -1) // { // ret = s.substr(begin, min_len); // } // return ret; //优化:数组模拟实现哈希表,count 标记“有效字符”的种类 int hash1[128] = { 0 }; //统计字符串 t 中每个字符出现的频次 int kinds = 0; //统计“有效字符”有多少种 for(auto c : t) { if(hash1[c]++ == 0) { kinds++; } } int left = 0; int right = 0; int count = 0; int min_len = s.size() + 1; int begin = -1; string ret; int hash2[128] = { 0 }; for(right = 0; right < s.size(); right++) { hash2[s[right]]++; //进窗口 if(hash2[s[right]] == hash1[s[right]]) { 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++; } } if(begin != -1) { ret = s.substr(begin, min_len); } return ret; } };

算法总结及流程解析:

结束语

       到此,15.串联所有单词的子串,16.最小覆盖子串 这两道算法题就讲解完了。《串联所有单词的子串》采用滑动窗口 + 哈希表方法,将单词视为字符处理,通过控制窗口步长和频次统计实现高效匹配;《最小覆盖子串》通过双哈希表动态维护目标串和窗口字符频次,优化版使用128位数组提升效率。希望大家能有所收获!

Read more

【C++】模板的两大特性

【C++】模板的两大特性

文章目录 * 前言 * 1. 关于 typename 的使用场景 * 2. 模板的分离编译问题 * 2.1 简述程序编译链接的过程 * 2.1.1 预处理 * 2.1.2 编译 * 2.1.3汇编 * 2.1.4 链接 * 2.2 模板分离编译为什么会链接报错 * 2.2.1 什么是分离编译 * 2.2.2 模板分离编译存在的问题 * 3. 解决办法 前言 本文探讨了C++模板编程中的两个关键问题。第一部分介绍了typename在模板中的特殊使用场景,指出当模板参数访问内嵌类型时必须使用typename关键字来消除编译器歧义。第二部分分析了模板分离编译导致链接错误的原因,通过对比普通函数和模板函数的编译链接过程,解释了模板定义必须放在头文件中才能被实例化的原理。文章结合代码示例和编译链接过程图解,帮助读者理解模板编译机制和常见错误的解决方法。 1.

By Ne0inhk
C++ 继承入门(下):友元、静态成员与菱形继承的底层逻辑

C++ 继承入门(下):友元、静态成员与菱形继承的底层逻辑

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》 《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心 ✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游 目录 前言 一. 友元 —— 友元关系不可继承   1、错误版本   2、正确版本 二. 静态成员 —— 继承体系中静态成员的共享性 三. 多继承及菱形继承问题:本质特点与解决方案   1、单继承与多继承模型   2、菱形继承:虚继承解决“数据冗余”与“二义性”     2.1 菱形继承出现的坑(解决二义性问题)     2.2 虚继承:彻底解决菱形继承问题     3、多继承中指针偏移问题 友元,静态成员,

By Ne0inhk
Re:从零开始的 C++ STL篇(七)二叉搜索树增删查操作系统讲解(含代码)+key/key-value场景联合分析

Re:从零开始的 C++ STL篇(七)二叉搜索树增删查操作系统讲解(含代码)+key/key-value场景联合分析

◆ 博主名称: 晓此方-ZEEKLOG博客大家好,欢迎来到晓此方的博客。⭐️C++系列个人专栏: 主题曲:C++程序设计⭐️ 踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰 0.1概要&序論 这里是「此方」,好久不见。 今天我们要学习的是二叉搜索树。它是在普通二叉树的基础上加入特定约束,从而具备了高效的搜索能力。虽然这种结构能够支持高效的插入、删除与查找操作,但其性能背后也隐藏着潜在的 效率风险 。同时,在 key 与 key-value 两种不同的应用场景 下,二叉搜索树的设计与实现方式也会产生不同的变化。这里是「此方」。让我们现在开始吧! 前情提要,没有系统学习过一般二叉树的小伙伴直接看这篇文章可能会有些吃力,此方在这里留一个传送门:Re:从零开始的链式二叉树:建树、遍历、计数、查找、判全、销毁全链路实现与底层剖析 一,二叉搜索树的概念

By Ne0inhk