【优选算法】(实战体验滑动窗口的奇妙之旅)

【优选算法】(实战体验滑动窗口的奇妙之旅)

🔥承渊政道:个人主页
❄️个人专栏: 《C语言基础语法知识》《数据结构与算法》《C++知识内容》《Linux系统知识》《算法刷题指南》《测评文章活动推广》
✨逆境不吐心中苦,顺境不忘来时路!✨🎬 博主简介:

在算法的世界里,“高效"永远是不变的追求,而优选算法的核心,就是在纷繁复杂的解题思路中,找到最简洁、最高效的解决方案.当我们面对数组、字符串的子区间问题时,常常会陷入暴力枚举的困境—双重循环带来的O(n²)时间复杂度,不仅会让代码运行效率低下,更会在数据量激增时陷入超时的僵局,成为算法进阶路上的"绊脚石”.而滑动窗口算法,正是破解这类问题的"神奇钥匙",它以其独特的动态窗口思想,将看似复杂的问题化繁为简,轻松实现时间复杂度从O(n²)到O(n)的跨越式优化,成为优选算法体系中不可或缺的核心工具之一.它就像一个可灵活移动的"观察框",通过双指针维护窗口的左右边界,在数据序列上有序滑动,动态调整窗口大小、更新窗口状态,无需重复计算,便能精准捕捉满足条件的子区间,这份"化繁为简"的魔力,正是它的奇妙之处所在.本次奇妙之旅,我们将跳出纯理论的框架,以实战为核心,手把手带你解锁滑动窗口的核心逻辑与应用技巧.无论你是刚接触算法的新手,还是想优化解题效率的进阶学习者,都能在这场实战之旅中,领略滑动窗口的独特魅力,掌握这一优选算法的实用技巧,让复杂的子区间问题迎刃而解.接下来,就让我们一起推开滑动窗口的大门,开启这场高效解题的奇妙探索吧!

目录

1.滑动窗口背景介绍

滑动窗口是双指针算法的经典分支,专门解决数组/字符串中连续子数组、子串的最优解问题.它的核心是:用两个指针界定一个动态的窗口,让窗口在序列上单向滑动,代替暴力枚举所有子序列,将时间复杂度从O(n²)优化到O(n)(线性时间).
窗口
由左指针left和右指针right界定的连续区间[left, right],窗口内的元素就是当前正在处理的连续子集.
滑动
两个指针只向右移动、绝不回退,窗口会像滑块一样在数组/字符串上从左滑到右,这是算法能做到线性时间的关键.
所有滑动窗口题目都遵循这个固定逻辑:
初始化
左指针left = 0,定义窗口状态变量(记录窗口内的和、字符计数、元素存在性等),定义结果变量.
扩张窗口
右指针 right 遍历整个序列,将当前元素加入窗口,更新窗口状态.
收缩窗口(动态窗口专属)
当窗口违反/满足题目条件时,移动左指针缩小窗口,同步更新窗口状态.
更新结果
在窗口滑动的过程中,记录最优解(最大/最小长度、和、值等).


2.长度最小的子数组(OJ题)

算法思路:解法一(暴力求解会超时)
从前往后枚举数组中的任意一个元素,把它当成起始位置.然后从这个起始位置开始,然后寻找一段最短的区间,使得这段区间的和大于等于目标值.
将所有元素作为起始位置所得的结果中,找到最小值即可.

核心代码

classSolution{public:intminSubArrayLen(int target, vector<int>& nums){//记录结果int ret = INT_MAX;int n = nums.size();//枚举出所有满⾜和⼤于等于 target 的⼦数组[start, end]//由于是取到最⼩,因此枚举的过程中要尽量让数组的⻓度最⼩//枚举开始位置for(int start =0; start < n; start++){int sum =0;//记录从这个位置开始的连续数组的和//寻找结束位置for(int end = start; end < n; end++){ sum += nums[end];//将当前位置加上if(sum >= target)//当这段区间内的和满⾜条件时{//更新结果,start 开头的最短区间已经找到 ret =min(ret, end - start +1);break;}}}//返回最后结果return ret == INT_MAX ?0: ret;}};

算法思路:解法二(滑动窗口)
由于此问题分析的对象是一段连续的区间,因此可以考虑滑动窗口的思想来解决这道题.
让滑动窗口满足:从 i 位置开始,窗口内所有元素的和小于 target(那么当窗口内元素之和第一次大于等于目标值的时候,就是 i 位置开始,满足条件的最小长度).
做法:将右端元素划入窗口中,统计出此时窗口内元素的和:

  • 如果窗口内元素之和大于等于 target:更新结果,并且将左端元素划出去的同时继续判断是否满足条件并更新结果(因为左端元素可能很小,划出去之后依旧满足条件)
  • 如果窗口内元素之和不满足条件:right++,另下一个元素进入窗口.

为何滑动窗口可以解决问题,并且时间复杂度更低?

  • 这个窗口寻找的是:以当前窗口最左侧元素(记为 left1)为基准,符合条件的情况.也就是在这道题中,从 left1 开始,满足区间和 sum >= target 时的最右侧(记为 right1)能到哪里.
  • 我们既然已经找到从 left1 开始的最优的区间,那么就可以大胆舍去 left1.但是如果继续像方法一一样,重新开始统计第二个元素(left2)往后的和,势必会有大量重复的计算(因为我们在求第一段区间的时候,已经算出很多元素的和了,这些和是可以在计算下次区间和的时候用上的).
  • 此时,right1 的作用就体现出来了,我们只需将 left1 这个值从 sum 中剔除.从 right1 这个元素开始,往后找满足 left2 元素的区间(此时 right1 也有可能是满足的,因为 left1 可能很小.sum 剔除掉 left1 之后,依旧满足大于等于 target).这样我们就能省掉大量重复的计算.
  • 这样我们不仅能解决问题,而且效率也会大大提升.

时间复杂度: 虽然代码是两层循环,但是我们的 left 指针和 right 指针都是不回退的,两者最多都往后移动 n 次.因此时间复杂度是 O(N).


核心代码

classSolution{public:intminSubArrayLen(int target, vector<int>& nums){int n = nums.size(), sum =0, len = INT_MAX;for(int left =0, right =0; right < n; right++){ sum += nums[right];//进窗⼝while(sum >= target)//判断{ len =min(len, right - left +1);//更新结果 sum -= nums[left++];//出窗⼝}}return len == INT_MAX ?0: len;}};

完整测试代码

#include<iostream>#include<vector>#include<algorithm>usingnamespace std;classSolution{public:intminSubArrayLen(int target, vector<int>& nums){int n = nums.size(), sum =0, len = INT_MAX;for(int left =0, right =0; right < n; right++){ sum += nums[right];// 右指针扩窗口while(sum >= target)// 满足条件,收缩左指针{ len =min(len, right - left +1);// 更新最小长度 sum -= nums[left++];// 左指针缩窗口}}//无满足条件的子数组返回0,否则返回最小长度return len == INT_MAX ?0: len;}};intmain(){ Solution sol;int target1 =7; vector<int> nums1 ={2,3,1,2,4,3}; cout <<"{2, 3, 1, 2, 4, 3}"<< sol.minSubArrayLen(target1, nums1)<< endl;int target2 =11; vector<int> nums2 ={1,1,1,1,1,1,1,1}; cout <<"{1,1,1,1,1,1,1,1}"<< sol.minSubArrayLen(target2, nums2)<< endl;int target3 =4; vector<int> nums3 ={1,4,4}; cout <<"{1,4,4}"<< sol.minSubArrayLen(target3, nums3)<< endl;return0;}

3.无重复字符的最长子串(OJ题)


算法思路:解法一(暴力求解➡️不会超时,可以通过)
枚举从每一个位置开始往后,无重复字符的子串可以到达什么位置.找出其中长度最大的即可.
在往后寻找无重复子串能到达的位置时,可以利用哈希表统计出字符出现的频次,来判断什么时候子串出现了重复元素.
核心代码

classSolution{public:intlengthOfLongestSubstring(string s){int ret =0;//记录结果int n = s.length();// 1.枚举从不同位置开始的最⻓重复⼦串//枚举起始位置for(int i =0; i < n; i++){//创建⼀个哈希表,统计频次int hash[128]={0};//寻找结束为⽌for(int j = i; j < n; j++){ hash[s[j]]++;//统计字符出现的频次if(hash[s[j]]>1)//如果出现重复的break;//如果没有重复,就更新 ret ret =max(ret, j - i +1);}}// 2. 返回结果return ret;}};

算法思路:解法二(滑动窗口)
研究的对象依旧是一段连续的区间,因此继续使用滑动窗口思想来优化.
让滑动窗口满足:窗口内所有元素都是不重复的.
做法:右端元素 ch 进入窗口的时候,哈希表统计这个字符的频次:

  • 如果这个字符出现的频次超过 1,说明窗口内有重复元素,那么就从左侧开始划出窗口,直到 ch 这个元素的频次变为 1,然后再更新结果.

如果没有超过 1,说明当前窗口没有重复元素,可以直接更新结果

核心代码

classSolution{public:intlengthOfLongestSubstring(string s){int hash[128]={0};// 使⽤数组来模拟哈希表int left =0, right =0, n = s.size();int ret =0;while(right < n){ hash[s[right]]++;//进⼊窗⼝while(hash[s[right]]>1)//判断 hash[s[left++]]--;//出窗⼝ ret =max(ret, right - left +1);//更新结果 right++;//让下⼀个元素进⼊窗⼝}return ret;}};

完整测试代码

// 必备头文件#include<iostream>#include<string>#include<algorithm>usingnamespace std;classSolution{public:intlengthOfLongestSubstring(string s){int hash[128]={0};//数组模拟哈希表,记录ASCII字符出现次数int left =0, right =0, n = s.size();int ret =0;//存储最长无重复子串长度while(right < n){ hash[s[right]]++;//右指针字符进入窗口while(hash[s[right]]>1)//出现重复字符,收缩窗口 hash[s[left++]]--;//左指针字符移出窗口 ret =max(ret, right - left +1);//更新最长长度 right++;//右指针右移,扩张窗口}return ret;}};intmain(){ Solution sol; string s1 ="abcabcbb"; cout <<"abcabcbb:"<< sol.lengthOfLongestSubstring(s1)<< endl; string s2 ="bbbbb"; cout <<"bbbbb:"<< sol.lengthOfLongestSubstring(s2)<< endl; string s3 ="pwwkew"; cout <<"pwwkew:"<< sol.lengthOfLongestSubstring(s3)<< endl; string s4 =""; cout <<""<< sol.lengthOfLongestSubstring(s4)<< endl; string s5 =" "; cout <<""<< sol.lengthOfLongestSubstring(s5)<< endl;return0;}

4.最大连续1的个数|||(OJ题)


算法思路:解法(滑动窗口)
不要去想怎么翻转,不要把问题想的很复杂,这道题的结果无非就是一段连续的 1 中间塞了 k0 嘛.因此,我们可以把问题转化成:求数组中一段最长的连续区间,要求这段区间内 0 的个数不超过 k 个.
算法流程
①初始化一个大小为 2 的数组就可以当做哈希表 hash 了;初始化一些变量 left = 0right = 0ret = 0
②当 right 小于数组大小的时候,一直下列循环:
(1)让当前元素进入窗口,顺便统计到哈希表中;
(2)检查 0 的个数是否超标:如果超标,依次让左侧元素滑出窗口,顺便更新哈希表的值,直到 0 的个数恢复正常;
(3)程序到这里,说明窗口内元素是符合要求的,更新结果;
(4)right++,处理下一个元素;
③循环结束后,ret 存的就是最终结果.

核心代码

classSolution{public:intlongestOnes(vector<int>& nums,int k){int ret =0;for(int left =0, right =0, zero =0; right < nums.size(); right++){if(nums[right]==0) zero++;//进窗⼝while(zero > k)//判断if(nums[left++]==0) zero--;//出窗⼝ ret =max(ret, right - left +1);//更新结果}return ret;}};

完整测试代码

#include<iostream>#include<vector>#include<algorithm>usingnamespace std;classSolution{public:intlongestOnes(vector<int>& nums,int k){int ret =0;for(int left =0, right =0, zero =0; right < nums.size(); right++){if(nums[right]==0) zero++;// 进窗口:右指针元素加入,统计0的个数while(zero > k)// 判断:0超标,收缩窗口if(nums[left++]==0) zero--;// 出窗口:左指针右移,减少0的个数 ret =max(ret, right - left +1);// 更新最大窗口长度}return ret;}};intmain(){ Solution sol; vector<int> nums1 ={1,1,1,0,0,0,1,1,1,1,0};int k1 =2; cout <<"{1,1,1,0,0,0,1,1,1,1,0}"<< sol.longestOnes(nums1, k1)<<" (预期:6)"<< endl; vector<int> nums2 ={0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1};int k2 =3; cout <<"{0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1}"<< sol.longestOnes(nums2, k2)<<" (预期:10)"<< endl; vector<int> nums3 ={1,0,1,1,0};int k3 =1; cout <<"{1,0,1,1,0}"<< sol.longestOnes(nums3, k3)<<" (预期:4)"<< endl; vector<int> nums4 ={1,1,1,1};int k4 =2; cout <<"{1,1,1,1}"<< sol.longestOnes(nums4, k4)<<" (预期:4)"<< endl; vector<int> nums5 ={0,0,0,0};int k5 =3; cout <<"{0,0,0,0}"<< sol.longestOnes(nums5, k5)<<" (预期:3)"<< endl;return0;}

5.将x减到0的最小操作数(OJ题)


算法思路:解法(滑动窗口)
题目要求的是数组左端+右端两段连续的、和为 x 的最短数组,信息量稍微多一些,不易理清思路;我们可以转化成求数组内一段连续的、和为 sum(nums) - x 的最长数组.此时,就是熟悉的滑动窗口问题了.
算法流程
①转化问题:求 target = sum(nums) - x.如果 target < 0,问题无解;
②初始化左右指针 l = 0,r = 0(滑动窗口区间表示为 [l, r),左右区间是否开闭很重要,必须设定与代码一致),记录当前滑动窗口内数组和的变量 sum = 0,记录当前满足条件数组的最大区间长度 maxLen = -1;
③当 r 小于等于数组长度时,一直循环:
(1)如果 sum < target,右移右指针,直至变量和大于等于 target,或右指针已经移到头;
(2)如果 sum > target,右移左指针,直至变量和小于等于 target,或左指针已经移到头;
(3)如果经过前两步的左右移动使得 sum == target,维护满足条件数组的最大长度,并让下个元素进入窗口;
④循环结束后,如果 maxLen 的值有意义,则计算结果返回;否则,返回 -1.


核心代码

classSolution{public:intminOperations(vector<int>& nums,int x){int sum =0;for(int a : nums) sum += a;int target = sum - x;//细节问题if(target <0)return-1;int ret =-1;for(int left =0, right =0, tmp =0; right < nums.size(); right++){ tmp += nums[right];//进窗⼝while(tmp > target)//判断 tmp -= nums[left++];//出窗⼝if(tmp == target)//更新结果 ret =max(ret, right - left +1);}if(ret ==-1)return ret;elsereturn nums.size()- ret;}};

完整测试代码

#include<iostream>#include<vector>#include<algorithm>usingnamespace std;classSolution{public:intminOperations(vector<int>& nums,int x){int sum =0;for(int a : nums) sum += a;int target = sum - x;//细节:数组总和小于x,无法减到0if(target <0)return-1;int ret =-1;//滑动窗口寻找和为target的最长连续子数组for(int left =0, right =0, tmp =0; right < nums.size(); right++){ tmp += nums[right];//进窗口while(tmp > target)//窗口和超标,收缩 tmp -= nums[left++];//出窗口if(tmp == target)//找到目标和,更新最长长度 ret =max(ret, right - left +1);}//无结果返回-1,否则总长度-最长子数组长度=最小操作数return ret ==-1?-1: nums.size()- ret;}};intmain(){ Solution sol; vector<int> nums1 ={1,1,4,2,3};int x1 =5; cout <<"{1,1,4,2,3}"<< sol.minOperations(nums1, x1)<<" (预期:2)"<< endl; vector<int> nums2 ={5,6,7,8,9};int x2 =4; cout <<"{5,6,7,8,9}"<< sol.minOperations(nums2, x2)<<" (预期:-1)"<< endl; vector<int> nums3 ={3,2,20,1,1,3};int x3 =10; cout <<"{3,2,20,1,1,3}"<< sol.minOperations(nums3, x3)<<" (预期:5)"<< endl; vector<int> nums4 ={1,2,3};int x4 =6; cout <<"{1,2,3}"<< sol.minOperations(nums4, x4)<<" (预期:3)"<< endl; vector<int> nums5 ={2,4,6};int x5 =13; cout <<"{2,4,6}"<< sol.minOperations(nums5, x5)<<" (预期:-1)"<< endl;return0;}

6.水果成篮(OJ题)


算法思路:解法(滑动窗口)
研究的对象是一段连续的区间,可以使用滑动窗口思想来解决问题.
让滑动窗口满足:窗口内水果的种类只有两种.
做法
右端水果进入窗口的时候,用哈希表统计这个水果的频次.这个水果进来后,判断哈希表的大小:
如果大小超过2:说明窗口内水果种类超过了两种.那么就从左侧开始依次将水果划出窗口,直到哈希表的大小小于等于2,然后更新结果;
如果没有超过 2,说明当前窗口内水果的种类不超过两种,直接更新结果 ret.
算法流程
①初始化哈希表 hash 来统计窗口内水果的种类和数量;
②初始化变量:左右指针 left = 0,right = 0,记录结果的变量 ret = 0;
③当 right 小于数组大小的时候,一直执行下列循环:
(1)将当前水果放入哈希表中;
(2)判断当前水果进来后,哈希表的大小:
• 如果超过2:
◦ 将左侧元素滑出窗口,并且在哈希表中将该元素的频次减一;
◦ 如果这个元素的频次减一之后变成了0,就把该元素从哈希表中删除;
◦ 重复上述两个过程,直到哈希表中的大小不超过2;
(3)更新结果 ret;
(4)right++,让下一个元素进入窗口;
④循环结束后,ret 存的就是最终结果.

核心代码

classSolution{public:inttotalFruit(vector<int>& f){ unordered_map<int,int> hash;//统计窗⼝内出现了多少种⽔果int ret =0;for(int left =0, right =0; right < f.size(); right++){ hash[f[right]]++;//进窗⼝while(hash.size()>2)//判断{//出窗⼝ hash[f[left]]--;if(hash[f[left]]==0) hash.erase(f[left]); left++;} ret =max(ret, right - left +1);}return ret;}};//可以用数组模拟一下哈希表从而优化一下时间效率classSolution{public:inttotalFruit(vector<int>& f){int hash[100001]={0};//统计窗⼝内出现了多少种⽔果int ret =0;for(int left =0, right =0, kinds =0; right < f.size(); right++){if(hash[f[right]]==0) kinds++;//维护⽔果的种类 hash[f[right]]++;//进窗⼝while(kinds >2)//判断{//出窗⼝ hash[f[left]]--;if(hash[f[left]]==0) kinds--; left++;} ret =max(ret, right - left +1);}return ret;}};

完整测试代码

#include<iostream>#include<vector>#include<unordered_map>#include<algorithm>usingnamespace std;classSolution{public:inttotalFruit(vector<int>& f){ unordered_map<int,int> hash;//哈希表:key=水果种类,value=窗口内数量int ret =0;//记录最大采摘数//滑动窗口双指针for(int left =0, right =0; right < f.size(); right++){ hash[f[right]]++;//进窗口:右指针水果加入,计数+1//窗口内水果种类超过2种,收缩左指针while(hash.size()>2){ hash[f[left]]--;//出窗口:左指针水果计数-1//计数为0,删除该种类(保证hash.size()准确)if(hash[f[left]]==0) hash.erase(f[left]); left++;}//更新最长合法窗口长度 ret =max(ret, right - left +1);}return ret;}};intmain(){ Solution sol; vector<int> nums1 ={1,2,1}; cout <<"{1,2,1}"<< sol.totalFruit(nums1)<<" (预期:3)"<< endl; vector<int> nums2 ={0,1,2,2}; cout <<"{0,1,2,2}"<< sol.totalFruit(nums2)<<" (预期:3)"<< endl; vector<int> nums3 ={1,2,3,2,2}; cout <<"{1,2,3,2,2}"<< sol.totalFruit(nums3)<<" (预期:4)"<< endl; vector<int> nums4 ={3,3,3,3}; cout <<"{3,3,3,3}"<< sol.totalFruit(nums4)<<" (预期:4)"<< endl; vector<int> nums5 ={0,1,0,1,0}; cout <<"{0,1,0,1,0}"<< sol.totalFruit(nums5)<<" (预期:5)"<< endl;return0;}

7.找到字符串中所有字母异位词(OJ题)


算法思路:解法(滑动窗口 + 哈希表)
因为字符串 p 的异位词的长度一定与字符串 p 的长度相同,所以我们可以在字符串 s 中构造一个长度为与字符串 p 的长度相同的滑动窗口,并在滑动中维护窗口中每种字母的数量;
当窗口中每种字母的数量与字符串 p 中每种字母的数量相同时,则说明当前窗口为字符串 p 的异位词;
因此可以用两个大小为 26 的数组来模拟哈希表,一个来保存 s 中的子串每个字符出现的个数,另一个来保存 p 中每一个字符出现的个数.这样就能判断两个串是否是异位词.

核心代码

classSolution{public: vector<int>findAnagrams(string s, string p){ vector<int> ret;int hash1[26]={0};//统计字符串 p 中每个字符出现的个数for(auto ch : p) hash1[ch -'a']++;int hash2[26]={0};//统计窗⼝⾥⾯的每⼀个字符出现的个数int m = p.size();for(int left =0, right =0, count =0; right < s.size(); right++){char in = s[right];//进窗⼝ + 维护 countif(++hash2[in -'a']<= hash1[in -'a']) count++;if(right - left +1> m)//判断{char out = s[left++];//出窗⼝ + 维护 countif(hash2[out -'a']--<= hash1[out -'a']) count--;}//更新结果if(count == m) ret.push_back(left);}return ret;}};

完整测试代码

#include<iostream>#include<vector>#include<string>usingnamespace std;classSolution{public: vector<int>findAnagrams(string s, string p){ vector<int> ret;int hash1[26]={0};//统计字符串 p 中每个字符出现的个数for(auto ch : p) hash1[ch -'a']++;int hash2[26]={0};//统计窗口里面的每一个字符出现的个数int m = p.size();for(int left =0, right =0, count =0; right < s.size(); right++){char in = s[right];//进窗口 + 维护 countif(++hash2[in -'a']<= hash1[in -'a']) count++;if(right - left +1> m)//判断:窗口长度超过p的长度,收缩{char out = s[left++];//出窗口 + 维护 countif(hash2[out -'a']--<= hash1[out -'a']) count--;}//更新结果:count等于p长度,说明窗口是p的异位词if(count == m) ret.push_back(left);}return ret;}};voidprintResult(vector<int>& res){ cout <<"[";for(int i =0; i < res.size(); i++){if(i >0) cout <<","; cout << res[i];} cout <<"]"<< endl;}intmain(){ Solution sol; string s1 ="cbaebabacd"; string p1 ="abc"; vector<int> res1 = sol.findAnagrams(s1, p1); cout <<"测试用例1结果:";printResult(res1);//预期:[0,6] cout <<"(预期:[0,6])"<< endl; string s2 ="abab"; string p2 ="ab"; vector<int> res2 = sol.findAnagrams(s2, p2); cout <<"测试用例2结果:";printResult(res2);//预期:[0,1,2] cout <<"(预期:[0,1,2])"<< endl; string s3 ="a"; string p3 ="ab"; vector<int> res3 = sol.findAnagrams(s3, p3); cout <<"测试用例3结果:";printResult(res3);//预期:[] cout <<"(预期:[])"<< endl; string s4 ="abc"; string p4 ="abc"; vector<int> res4 = sol.findAnagrams(s4, p4); cout <<"测试用例4结果:";printResult(res4);//预期:[0] cout <<"(预期:[0])"<< endl;return0;}

8.串联所有单词的子串(OJ题)


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

核心代码

classSolution{public: vector<int>findSubstring(string s, vector<string>& words){ vector<int> ret; unordered_map<string,int> hash1;//保存 words ⾥⾯所有单词的频次for(auto& s : words) hash1[s]++;int len = words[0].size(), m = words.size();for(int i =0; i < len; i++)//执⾏ len 次{ unordered_map<string,int> hash2;//维护窗⼝内单词的频次for(int left = i, right = i, count =0; right + len <= s.size(); right += len){//进窗⼝ + 维护count string in = s.substr(right, len); hash2[in]++;if(hash1.count(in)&& hash2[in]<= hash1[in]) count++;//判断if(right - left +1> len * m){//出窗⼝ + 维护 count 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;}};

完整测试代码

#include<iostream>#include<vector>#include<string>#include<unordered_map>usingnamespace std;classSolution{public: vector<int>findSubstring(string s, vector<string>& words){ vector<int> ret; unordered_map<string,int> hash1;//保存 words 里面所有单词的频次for(auto& s : words) hash1[s]++;int len = words[0].size(), m = words.size();for(int i =0; i < len; i++)//执行 len 次{ unordered_map<string,int> hash2;//维护窗口内单词的频次for(int left = i, right = i, count =0; right + len <= s.size(); right += len){//进窗口 + 维护count string in = s.substr(right, len); hash2[in]++;if(hash1.count(in)&& hash2[in]<= hash1[in]) count++;// 判断if(right - left +1> len * m){//出窗口 + 维护count 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;}};voidprintResult(vector<int>& res){ cout <<"[";for(int i =0; i < res.size(); i++){if(i >0) cout <<","; cout << res[i];} cout <<"]"<< endl;}intmain(){ Solution sol; string s1 ="barfoothefoobarman"; vector<string> words1 ={"foo","bar"}; vector<int> res1 = sol.findSubstring(s1, words1); cout <<"测试用例1结果:";printResult(res1);// 预期:[0,9] cout <<"(预期:[0,9])"<< endl; string s2 ="wordgoodgoodgoodbestword"; vector<string> words2 ={"word","good","best","word"}; vector<int> res2 = sol.findSubstring(s2, words2); cout <<"测试用例2结果:";printResult(res2);// 预期:[] cout <<"(预期:[])"<< endl; string s3 ="barbarfoo"; vector<string> words3 ={"bar","foo"}; vector<int> res3 = sol.findSubstring(s3, words3); cout <<"测试用例3结果:";printResult(res3);// 预期:[3] cout <<"(预期:[3])"<< endl; string s4 ="fooba"; vector<string> words4 ={"foo","bar"}; vector<int> res4 = sol.findSubstring(s4, words4); cout <<"测试用例4结果:";printResult(res4);// 预期:[] cout <<"(预期:[])"<< endl;return0;}

9.最小覆盖子串(OJ题)


算法思路:解法(滑动窗口+哈希表)
研究对象是连续的区间,因此可以尝试使用滑动窗口的思想来解决.
如何判断当前窗口内的所有字符是符合要求的呢?
我们可以使用两个哈希表,其中一个将目标串的信息统计起来,另一个哈希表动态的维护窗口内字符串的信息.
当动态哈希表中包含目标串中所有的字符,并且对应的个数都不小于目标串的哈希表中各个字符的个数,那么当前的窗口就是一种可行的方案.
算法流程
①定义两个全局的哈希表:1号哈希表 hash1 用来记录子串的信息,2号哈希表 hash2 用来记录目标串 t 的信息;
②实现一个接口函数,判断当前窗口是否满足要求:
(1)遍历两个哈希表中对应位置的元素:
如果 t 中某个字符的数量大于窗口中字符的数量,也就是 2 号哈希表某个位置大于 1 号哈希表,说明不匹配,返回 false;
如果全都匹配,返回 true.
主函数中
①先将 t 的信息放入 2 号哈希表中;
②初始化一些变量:左右指针:left = 0,right = 0;目标子串的长度:len = INT_MAX;目标子串的起始位置:retleft;(通过目标子串的起始位置和长度,我们就能找到结果)
③当 right 小于字符串 s 的长度时,一直下列循环:
(1)将当前遍历到的元素扔进 1 号哈希表中;
(2)检测当前窗口是否满足条件:
如果满足条件:
◦ 判断当前窗口是否变小.如果变小:更新长度 len,以及字符串的起始位置 retleft;
◦ 判断完毕后,将左侧元素滑出窗口,顺便更新 1 号哈希表;
◦ 重复上面两个过程,直到窗口不满足条件;
(3)right++,遍历下一个元素;
④判断 len 的长度是否等于 INT_MAX:
(1)如果相等,说明没有匹配,返回空串;
(2)如果不相等,说明匹配,返回 s 中从 retleft 位置往后 len 长度的字符串.

核心代码

classSolution{public: string minWindow(string s, string t){int hash1[128]={0};//统计字符串t中每⼀个字符的频次int kinds =0;//统计有效字符有多少种for(auto ch : t)if(hash1[ch]++==0) kinds++;int hash2[128]={0};//统计窗⼝内每个字符的频次int minlen = INT_MAX, begin =-1;for(int left =0, right =0, count =0; right < s.size(); right++){char in = s[right];if(++hash2[in]== hash1[in]) count++;//进窗⼝ + 维护 countwhile(count == kinds)//判断条件{if(right - left +1< minlen)//更新结果{ minlen = right - left +1; begin = left;}char out = s[left++];if(hash2[out]--== hash1[out]) count--;//出窗⼝ + 维护count}}if(begin ==-1)return"";elsereturn s.substr(begin, minlen);}};

完整测试代码

#include<iostream>#include<string>#include<climits>usingnamespace std;classSolution{public: string minWindow(string s, string t){int hash1[128]={0};//统计字符串t中每一个字符的频次int kinds =0;//统计有效字符有多少种for(auto ch : t)if(hash1[ch]++==0) kinds++;int hash2[128]={0};//统计窗口内每个字符的频次int minlen = INT_MAX, begin =-1;for(int left =0, right =0, count =0; right < s.size(); right++){char in = s[right];if(++hash2[in]== hash1[in]) count++;//进窗口 + 维护countwhile(count == kinds)//判断条件:窗口满足要求{if(right - left +1< minlen)//更新最小窗口{ minlen = right - left +1; begin = left;}char out = s[left++];if(hash2[out]--== hash1[out]) count--;//出窗口 + 维护count}}if(begin ==-1)return"";elsereturn s.substr(begin, minlen);}};intmain(){ Solution sol; string s1 ="ADOBECODEBANC"; string t1 ="ABC"; cout <<"测试用例1结果:"<< sol.minWindow(s1, t1)<<" (预期:BANC)"<< endl; string s2 ="a"; string t2 ="a"; cout <<"测试用例2结果:"<< sol.minWindow(s2, t2)<<" (预期:a)"<< endl; string s3 ="a"; string t3 ="aa"; cout <<"测试用例3结果:\""<< sol.minWindow(s3, t3)<<"\" (预期:空串)"<< endl; string s4 ="aa"; string t4 ="aa"; cout <<"测试用例4结果:"<< sol.minWindow(s4, t4)<<" (预期:aa)"<< endl;return0;}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容:【优选算法】(实战感悟二分查找算法的思想原理)


每日心灵鸡汤:我依旧待人真诚,但不再给予厚望!
我从不后悔对任何人好,哪怕是看错人,哪怕是撞南墙,我愿意为我的选择买单,也不后悔我的任何决定,只是因为我很好,樱花树下站谁都美,我的爱给谁都热烈,我爱谁谁才会特别.
不将别人的给予当成理所当然,更不怀疑自己的善良和真诚,应当反省的从来都是自己的眼光和见识.人性如此幽深复杂,千帆过尽,我变得什么都能理解,每次的真诚让我觉得踏实.所以在失去的时候,有遗憾的不该是我.

Read more

【初阶数据结构08】——树的基本概念与堆的基本功能实现

【初阶数据结构08】——树的基本概念与堆的基本功能实现

文章目录 前言 一、树的概念 1.1 树的定义 1.2 树的相关术语 1.3 树的表示 1.4 树在实际中的应用 二、二叉树概念及结构 2.1 二叉树的定义 2.2 现实中的二叉树 2.3 特殊的二叉树 2.4 二叉树的性质 2.5 二叉树的存储结构 1. 顺序存储 2. 链式存储 三、堆的概念与结构 3.1 堆的定义 3.2 堆的存储结构 四、堆的基本功能实现 4.1 辅助函数:

By Ne0inhk
【递归,搜索与回溯算法 & floodfill 算法】深入理解 floodfill 算法:floodfill 算法小专题

【递归,搜索与回溯算法 & floodfill 算法】深入理解 floodfill 算法:floodfill 算法小专题

图像渲染     题目解析         算法原理         解法:暴搜          模拟过程          递归过程:        回溯过程:       处理细节问题     但是如果在上述矩阵的情况下,给我们的 color 不是 2 ,而是 1,也就是原始像素值和要修改像素值相同的情况,此时很有可能在递归的时候走重复路径: 我们不处理好这种特殊的情况,就很容易会写出 bug;所以在编写代码的时候,我们先判断一下,if (image[sr][sc]==color),直接返回即可,因为无需修改;      编写代码      报错原因:没有修改 image[ sr ][ sc ] 为 color 优化:本题并不需要 vis 数组来标记走过的格子,因为走过的格子都会修改,修改后会被剪枝条件筛查掉,并且这道题也没有递归出口,也不需要恢复现场; 岛屿数量     题目解析         算法原理         解法:

By Ne0inhk
力扣校招算法通关:双指针技巧全场景拆解 —— 从数组操作到环检测的高效解题范式

力扣校招算法通关:双指针技巧全场景拆解 —— 从数组操作到环检测的高效解题范式

文章目录 * 前言 * 双指针 * 例题讲解 * 移动零 力扣 * 复写零 力扣 * 快乐数 力扣 * 盛最多水的容器 力扣 * 有效三角形的个数 力扣 * 查找总价格为目标值的两个商品 力扣 * 三数之和 力扣 前言 在力扣校招算法题中,双指针技巧是一类高频且实用的解题方法。它并非真正的 “指针”,而是通过两个数组下标(或迭代器)的协同移动,在数组划分、区间求解、环检测等场景中实现高效遍历与逻辑处理,往往能将时间复杂度从暴力法的 O(n平方)优化至O(n),是校招笔试和面试中突破数组类难题的关键武器。 本专栏将围绕力扣校招高频的双指针题型展开,从 “移动零”“复写零” 的数组操作,到 “快乐数” 的环检测、“盛最多水的容器” 的区间优化,再到 “三数之和” 的多指针协同,逐一拆解双指针的核心逻辑、边界处理与去重技巧,

By Ne0inhk
【数据结构】二叉树初阶——超详解!!!(包含二叉树的实现)

【数据结构】二叉树初阶——超详解!!!(包含二叉树的实现)

【数据结构】二叉树初阶——超详解!!!(包含二叉树的实现) * 前言 * 一、树是什么? * 1. 树的定义 * 2. 一些常见术语 * 二、二叉树 * 1. 二叉树是什么 ? * 2. 二叉树的组成 * 2. 特殊的二叉树 * 3. 二叉树的顺序存储(完全二叉树) * 4. 二叉树的一些性质 * 三、二叉树的实现(重点!!!) * 1. 二叉树的链式存储(非完全二叉树) * 2. 实现思路 * 3. 代码实现 * (1)创建头文件&源文件 * (2)定义二叉树(定义) * (3)构建二叉树 * (4)二叉树遍历(前中后序) * (5)二叉树的层序遍历 * (6)二叉树节点个数的计算

By Ne0inhk