深度解析算法之滑动窗口

发布于:2025-04-01 ⋅ 阅读:(18) ⋅ 点赞:(0)

12滑动窗口—将 x 减到 0 的最小操作数

题目传送门

题目描述:

给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。
如果可以将 x 恰好 减到 0 ,返回 最小操作数 ;否则,返回 -1 。

示例 1:

输入 nums = [1,1,4,2,3], x = 5
输出: 2
解释 最佳解决方案是移除后两个元素,将 x 减到 0 。

示例 2:
输入 nums = [5,6,7,8,9], x = 4
输出:-1

示例 3:

输入 nums = [3,2,20,1,1,3], x = 10
输出: 5
解释: 最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。

这里我们通过示例1里面的数据,我们可以发现最优的操作次数是2次image.png

对于示例2来说的话,我们不论是从左边还是右边进行删除操作,我们的x永远不可能到0的,所以我们返回-1

现在我们将问题进行转换:原来要求的是我们从最左边或者是最右边进行x删除元素,直到x变成0。那么我们可以将这个进行区域划分,如下图,左边的区域是a,右边的区域是b,a区域和b区域里面的数值加起来等于x,那么我们中间剩下的就是我们整个数组的值减去x,题目让我们求出最小的操作次数,那么就是说我们a+b最短的情况,换种思路,就是中间区域最长的情况,即sum-x,我们只要求出整个数组中和为sum-x的区域的最长值就行了
image.png
找出最长的子数组的长度,子数组的所有元素的和正好等于sum-x

做这种题我们可以想对立面

我们可以先想想 暴力解法:我们设置两个指针,左和右,我们让两个指针一开始指向起点,然后让右指针进行移动操作,直到我们的两个指针的区域间的值大于等于sum-x,我们就更新下我们的操作次数,然后移动我们的左指针继续进行更新的操作image.png
当我们的左指针往右边移动了,那么我们的右指针就没有必要往左边进行移动了,因为我们左指针向右移动过后,我们的两个指针区间中的元素大小更小了,我们右指针只能往右移动

那么我们现在在这个暴力解法的基础上进行优化
image.png

class Solution {

public:

    int minOperations(vector<int>& nums, int x)

    {

        int sum =0;

        for(int a:nums)

        {

            sum+=a;

        }

        int target =sum-x;

  

        //细节问题,target的值不能小于0

        if(target<0) return -1;

  

        int ret=-1;

        for(int left=0,right=0,tmp=0;right<nums.size();right++)

        {

            //tmp来计算我们左右指针中间的值的大小的

            tmp+=nums[right];//进窗口的操作

            while(tmp>target)//进行判断,如果区间中的值大于了target我们就停下来

            {

                tmp-=nums[left];

                left++;//出窗口

            }

            if(tmp==target)//更新结果

            {

                //更新我们两个指针中间的长度

                ret=max(ret,right-left+1);

            }

        }

        if(ret==-1) return ret;//没有找到

        else return nums.size()-ret;//找到的话我们直接返回我们的这个总长度减去我们中间的值

    }

};

13滑动窗口—水果成篮

题目传送门
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

示例 1:

输入: fruits = [1,2,1 ]
输出 3
解释: 可以采摘全部 3 棵树。

示例 2:

输入: fruits = [0,_1,2,2]
输出: 3
解释: 可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。

示例 3:

输入: fruits = [1,_2,3,2,2]
输出: 4
解释: 可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。

示例 4:

输入: fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出: 5
解释: 可以采摘 [1,2,1,1,2] 这五棵树。

我们现在有两个篮子进行摘水果,然后每个篮子只能装一个水果,不能装多种水果,我们现在按实例2来说,示例二我们先采摘0号水果,然后再采摘1号水果,然后我们就不能采摘了,因为再后面的2号水果和1号水果种类不同,所以我们是只能采摘2棵树,但是我们如果从1号开始采摘的话,我们然后可以摘2号水果,我们再还能接着采摘2号水果,所以我们这种方案是可以采摘3棵树的

就是我们的两个篮子只能装两种类型的水果,就是一个区间只能存在两种数字,不能存在第三种数字

转化: 找出一个最长的子数组,子数组里面不超过两种类型的水果

暴力解法:直接将所有的子数组都给找出来,找出其中最长的一个
我们可以借助一个哈希表来记录我们当前出现水果种类的次数

我们现在定义两个指针,左和右,两个指针都从最开始的位置行动,右指针保持运行,直到我们水果的种类大于2了,我们就停下来,那么此时左和右中间的区域就是我们要求的
然后我们的左指针往右运行,那么我们就得想想我们的右指针回不回来呢?是不是和左指针重新开始还是说直接从上次的位置开始
我们的左指针往右运行,那么我们水果种类只能出现两种:不变或者是变小,绝不可能变大,因为我们左往右走了一步,水果少了一个

如果这个在左指针运行后,我们水果的种类不变的话,我们右指针就没必要品回来了

如果水果种类减少的话,那么我们区间里面还是只存在一种类型的结果,我们的右指针也是没有必要回去的,右指针直接右移image.png
那么我们就可以使用滑动窗口进行解答了

进窗口的操作就是将我们右指针指向的元素丢到哈希表中
如果这个哈希表的长度大于2的话,我们就进行出窗口的操作
在哈希表中,我们不仅仅存入这个水果的种类,还要存入水果的数量

在出窗口的时候我们还要对我们哈希表中的元素进行判断,如果某种种类的水果数量变成了0的话,那么我们要将这个水果从哈希表中删除的

下面是我们的代码

class Solution {

public:

    int totalFruit(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)//进行判断,查看我们哈希表中的水果种类是否大于2

            {

                //出窗口操作

                hash[f[left]]--;

                //因为我们当前left要往右边走,那么我们就得判断下我们这个减少数量的这个水果的个数是否变成了0

                if(hash[f[left]]==0)//如果这个水果数量变成了0的话,那么我们就得将这个水果从哈希表中删除了

                {

                    hash.erase(f[left]);//将这个种类变为0的水果从哈希表中删除

                }

                left++;//左指针往右边走

            }

            ret=max(ret,right-left+1);//更新结果,结果就是我们左右指针中间的区间的长度

        }

        return ret;

    }

};

image.png
但是我们发现我们的这个时间复杂度不行,因为我们哈希表中插入删除元素
所以我们这里是可以进行优化的操作的,对当前的哈希表进行优化操作

我们可以直接利用一个数组来创建哈希表,并且增加一个变量kinds来表示我们水果的种类
下面我们就是直接利用数组进行操作

class Solution {

public:

    int totalFruit(vector<int>& f)

    {

        int hash[100001]={0};//统计窗口内出现多少种水果

  

        int ret=0;

        for(int left=0,right=0,kinds=0;right<f.size();right++)

        {

            //如果当前right指针指向的水果的个数为0的话,我们就种类++

            if(hash[f[right]]==0) kinds++;//维护水果的种类

            hash[f[right]]++;//进窗口

            while(kinds>2)//进行判断,查看我们哈希表中的水果种类是否大于2

            {

                //出窗口操作

                hash[f[left]]--;

                //因为我们当前left要往右边走,那么我们就得判断下我们这个减少数量的这个水果的个数是否变成了0

                if(hash[f[left]]==0)//如果这个水果数量变成了0的话,我们直接让种类减一

                {

                    kinds--;

                }

                left++;//左指针往右边走

            }

            ret=max(ret,right-left+1);//更新结果,结果就是我们左右指针中间的区间的长度

        }

        return ret;

    }

};

14滑动窗口_找到字符串中所有字母异位词

题目链接

给定两个字符串 s 和 p,找到 s 中所有 p 的

异位词

的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

示例 1:

输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的异位词。

示例 2:

输入: s = “abab”, p = “ab”
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的异位词

我们需要快速判断两个字符串是异位词,我们可以通过比较两个字符串来比较判断是否是异位词
image.png
操作方式就是利用哈希表来进行计数
下方就是暴力解法:不断的比较
image.png

一次移动len个子串然后和另一个子串进行比较,那么我们这里是可以用滑动窗口进行解答的
image.png

利用count来统计窗口中有效字符的个数
进窗口后我们考虑下我们进窗口的这个数据是否是有效的数据,这个字母的出现个数是否个另一个字符串里面对应字母的出现次数相等

出窗口之前我们判断下我们出去这个字符是否是有效字符,是有效字符的话那么我们就进行count–操作

然后我们更新结果

class Solution {

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];

            //进窗口

            hash2[in-'a']++;//统计对应字符出现的个数

  

            if(hash2[in-'a']<=hash1[in-'a']) count++;//如果s中的这个字符的出现次数小于p中字符出现的个数的话,那么有效字符要++的

            if(right-left+1>m)//判断,这个就是窗口大了,我们得进行出窗口的操作

            {

                //出窗口&&维护count

                char out=s[left++];//将这个元素丢出去,将这个元素丢出去,我们的left往右走

                if(hash2[out-'a']<=hash1[out-'a']) count--;//如果满足条件的话,那么就说明当前滑出去的是一个有效字符

                hash2[out-'a']--;

            }

  

            //更新

            if(count==m) ret.push_back(left);//直接将起始位置的下标进行输出

  

        }

        return ret;

    }

};

说白了就是一直维持这个count的大小,然后知道count的大小等于这个p字符串的长度了,我们就将起始的左指针进行返回的操作

15滑动窗口—串联所有单词的子串

题目链接

给定一个字符串 s 和一个字符串数组 words words 中所有字符串 长度相同

s 中的 串联子串 是指一个包含  words 中所有字符串以任意顺序排列连接起来的子串。

  • 例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd""cdabef", "cdefab""efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。

返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。

示例 1:

输入: s = “barfoothefoobarman”, words = [“foo”,“bar”]
输出:[0,9]
解释: 因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 “barfoo” 开始位置是 0。它是 words 中以 [“bar”,“foo”] 顺序排列的连接。
子串 “foobar” 开始位置是 9。它是 words 中以 [“foo”,“bar”] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。

示例 2:

输入: s = “wordgoodgoodgoodbestword”, words = [“word”,“good”,“best”,“word”]
输出 []
解释: 因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。
s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。
所以我们返回一个空数组。

示例 3:

输入: s = “barfoofoobarthefoobarman”, words = [“bar”,“foo”,“the”]
输出:[6,9,12]
解释: 因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。
子串 “foobarthe” 开始位置是 6。它是 words 中以 [“foo”,“bar”,“the”] 顺序排列的连接。
子串 “barthefoo” 开始位置是 9。它是 words 中以 [“bar”,“the”,“foo”] 顺序排列的连接。
子串 “thefoobar” 开始位置是 12。它是 words 中以 [“the”,“foo”,“bar”] 顺序排列的连接。

我们可以将题目进行转换,在左边的字符串中找到我们右边的a b的异位词就行了,那么这个题就很像之前的那个找到字符串中所有字母异位词这个题了
image.png
找到一个字符串就返回起始位置

那么这个题我们就会使用到滑动窗口和哈希表了

相较于找到字符串中所有字母异位词这个题的话,我们有三点不同

  • 哈希表
    之前的那题我们里面的哈希表存储的是一个个的字符,这里的话我们是一个又一个的字符串

  • left和right指针的移动
    因为我们字符串数组里面的字符串的长度都是相等的,那么我们在移动的时候为了让新的字符串进入到窗口的话,那么我们每次移动的话是需要前进len个距离,len就是我们单个字符串的长度
    移动的步长是每个单词的长度

  • 滑动窗口的执行次数
    我们滑动窗口执行的次数是len次
    因为我们的指针不仅仅是从第一个位置开始的,也可能从第二个位置,甚至第len个位置,如果从len+1的位置开始的话,那么就是重复了,和从第一个位置开始的情况重叠了,所以我们进行滑动窗口的次数是len次
    image.png

class Solution {

public:

    vector<int> findSubstring(string s, vector<string>& words)

    {

        vector<int> ret;

        //创建一个哈希表并且将单词在word中出现的次数进行记录

        unordered_map<string, int> hash1;;//保存words里面所有的单词的频次

        for(auto& s:words) hash1[s]++;//记录words里面所有的单词出现的频次

  

        //计算单词的长度

        int len=words[0].size(),m=words.size();//多少个单词

        //我们这里的滑动窗口是需要执行len次的

        for(int i=0;i<len;i++)//执行len次滑动窗口

        {

            unordered_map<string,int>hash2;//维护滑动窗口内单词的频次

            //因为我们right在移动到对应的位置后,我们需要将right+len个字符放到滑动窗口里面去

            for(int left=i,right=i,count=0;right+len<=s.size();right+=len)//count就是有效字符串的个数了

            {

                //进窗口+维护count

                string in =s.substr(right,len);//直接获取right后面len个字符,那么in就是一个字符串了

                hash2[in]++;//我们将in这个字符出现的频次进行更新

                if(hash2[in]<=hash1[in]) count++;//如果我们hash2这个哈希中当前字符串出现的频次小于hash1的话,说明是有效单词,如果我们哈希2里面这个字符串出现次数大于哈希1的话,那么这个肯定是一个无效的单词,因为hash1中都没有进行记录

                //判断

                if(right-left+1>len*m)//如果我们当前左右指针的范围大于我们要找的子串的长度的话,那么我们就进行出窗口操作

                {

                    //出窗口,维护count

                    string out=s.substr(left,len);//我们将left后面三个单词的范围的这个字符串拿出来

                    if(hash2[out]<=hash1[out]) count--;//有效单词减一

                    hash2[out]--;

  

                    //出完窗口之后,我们的left需要向后移动

                    left+=len;

                }

  
  

                //更新结果

                if(count==m) ret.push_back(left);//将我们

            }

  

        }

        //当整个循环结束之后,我们ret中存储的就是结果了

        return ret;

  

    }

};

我们先创建一个哈希1,用来存储我们words中的单词出现的次数,利用范围for获得每个单词出现的频次

然后我们再计算len就是每个单词的长度,因为这个题中每个单词的长度都是一样的
然后我们再来一个for循环,次数就是len次,因为我们的左指针有效循环的次数就是len次,如果是len+1次的话,那么结果是和第一次的结果是相同的

在这个for循环中我们再来一个for循环进行进窗口的操作,在进窗口之前我们需要创建一个哈希2来记录我们滑动窗口内单词的频次

这个内循环的我们需要维护好count,就是窗口内有效单词的数量,如果没有在哈希1中出现过的,统统都是无效单词,出现过的才算是有效单词

然后我们进行进窗口的操作,利用substr获取我们right后面len个范围的单词,将这个字符串放到in中,然后给hash2记录这个单词出现的次数,我们在进窗口后我们维护好我们的count,如果当前的单词在hash2中出现的次数小于hash1中出现的次数的话,那么就证明这个单词是有效的,为什么呢?因为如果hash2中这个单词出现的次数大于hash1中的话,那么这个单词就是无效的,在hash1中不存在的,所以hash2中出现次数小于等于hash1中单词的次数的话,那么在这个单词就是有效的单词,那么我们count++

然后我们进行判断,如果我们当前的right和left的范围大于我们words中字符串组合成的字符串的长度的话,那么我们就进行出窗口的操作,我们就得将我们的left往右边进行移动了,还是利用substr获取我们left后面len个字符的字符串,将这个字符串存在out中,然后将hash2中的out这个字符串给减一,在减一之前我们还需要维护下我们的count,对我们这个单词进行判断,是否是有效的单词

出完窗口后,我们的left需要往右边进行移动,移动的距离是len
然后我们进行结果的更新,如果我们count=m的话,那么我们将此时的left,就是这个对应字符串的开始位置存储子啊我们的vector容器中去

最后直接返回了

但是吧,我们当前的这个效率比较慢,因为我们在判断当前的字符串是否是有效的字符串的时候,如果我们这个字符串in在hash1中不存在 的话,那么我们就得进行创建hash1[in]了,所以这里是存在效率变低的,所以我们可以在判断条件中加上hash1.count(in)来判断我们的这个单词的数量,如果是0的话,我们 直接退出了这个条件判断了

class Solution {

public:

    vector<int> findSubstring(string s, vector<string>& words)

    {

        vector<int> ret;

        //创建一个哈希表并且将单词在word中出现的次数进行记录

        unordered_map<string, int> hash1;;//保存words里面所有的单词的频次

        for(auto& s:words) hash1[s]++;//记录words里面所有的单词出现的频次

  

        //计算单词的长度

        int len=words[0].size(),m=words.size();//多少个单词

        //我们这里的滑动窗口是需要执行len次的

        for(int i=0;i<len;i++)//执行len次滑动窗口

        {

            unordered_map<string,int>hash2;//维护滑动窗口内单词的频次

            //因为我们right在移动到对应的位置后,我们需要将right+len个字符放到滑动窗口里面去

            for(int left=i,right=i,count=0;right+len<=s.size();right+=len)//count就是有效字符串的个数了

            {

                //进窗口+维护count

                string in =s.substr(right,len);//直接获取right后面len个字符,那么in就是一个字符串了

                hash2[in]++;//我们将in这个字符出现的频次进行更新

                if(hash1.count(in)&&hash2[in]<=hash1[in]) count++;//如果我们hash2这个哈希中当前字符串出现的频次小于hash1的话,说明是有效单词,如果我们哈希2里面这个字符串出现次数大于哈希1的话,那么这个肯定是一个无效的单词,因为hash1中都没有进行记录

                //判断

                if(right-left+1>len*m)//如果我们当前左右指针的范围大于我们要找的子串的长度的话,那么我们就进行出窗口操作

                {

                    //出窗口,维护count

                    string out=s.substr(left,len);//我们将left后面三个单词的范围的这个字符串拿出来

                    if(hash1.count(out)&&hash2[out]<=hash1[out]) count--;//有效单词减一

                    hash2[out]--;

  

                    //出完窗口之后,我们的left需要向后移动

                    left+=len;

                }

  
  

                //更新结果

                if(count==m) ret.push_back(left);//将我们

            }

  

        }

        //当整个循环结束之后,我们ret中存储的就是结果了

        return ret;

  

    }

};

16滑动窗口—最小覆盖子串

题目链接
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入: s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
解释: 最小覆盖子串 “BANC” 包含来自字符串 t 的 ‘A’、‘B’ 和 ‘C’。

示例 2:

输入: s = “a”, t = “a”
输出:“a”
解释: 整个字符串 s 是最小覆盖子串。

示例 3:

输入: s = “a”, t = “aa”
输出: “”
解释: t 中两个字符 ‘a’ 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

只要我们的s中可以将t全部涵盖了的话,那么就是符合要求的
使用暴力解决,如果hash2中的ABC的次数大于hash1中的次数的话,那么hash2里面存的这一段字符串就是有效的字符串了

image.png
然后接下来从D这个位置接着枚举,就这种暴力解决的方法

解法一:暴力枚举+哈希表

那么我们是否可以在这个解法的基础上进行优化呢
我们在找到符合要求的一组之后,我们的left往右走,那么此时我们的right是没有必要回去的
image.png
如果我们原先left指向的元素他的出现次数太多了或者是一个无效的,那么我们的left往右边移动一位,和此时right组成的区间依旧是有效的

但是如果我们原先left指向的元素是一个有效字符并且出现次数的小于我们hash1中的次数,那么我们此时的区间就是不符合要求的,我们的right依旧不用回来,继续向后面走,直到找到了符合要求的那个元素为止

那么优化的方案就是滑动窗口+哈希表

当我们的窗口合法之后我们再进行出窗口的操作image.png
优化操作
我们使用count标记有效字符的种类 ,我们这里需要hash2中出现hash1中所有种类的字符,而不是个数,我们一定要按照有效字符种类来进行操作

我们进窗口的时候维护一下count,出窗口的时候维护一下count,判断的时候判断一下count

class Solution {

public:

    string minWindow(string s, string t)

    {

        int hash1[128]={0};//统计t字符串中每一个字符出现的频次

  

        int kinds=0;//统计有效字符有多少种

  

        for(auto ch:t)

        {

            //如何判断我们当前的有效字符种类是否增加了呢?

            //如果在进窗口前这个字符数量为0 的话,并且是有效字符的话,那么我们的种类就多了一种了

            if(hash1[ch]==0) kinds++;//有效字符的种类加一

            hash1[ch]++;

        }

        //出了循环我们就统计到了我们字符的种类数量

  

        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];//获取我们这个字符的信息

            hash2[in]++;//给这个字符的次数++

            //判断我们加入的字符是否是有效字符

            //进窗口+维护count变量

            if(hash2[in]==hash1[in]) count++;//说明我们加入了一个有效的字符

  

            //判断条件

            while(count==kinds)

            {

                //更新结果

                if(right-left+1<minlen)//说明此时我们需要更新结果了

                {

                    //计算最小的子串的长度

                    minlen=right-left+1;

                    //开始的指针指向我们的left

                    begin=left;

                }

                //出窗口

                char out =s[left];//记录当前出窗口的单词

                left++;//左指针往右边走

  

                //维护count

                if(hash2[out]==hash1[out])count--;//说明我们当前的字符是有效字符

                hash2[out]--;//将当前出窗口的字符进行--操作

            }

        }

        if(begin==-1) return "";//如果我们开始的指针没有变的话,那么我们就直接返回空就行了

        else return s.substr(begin,minlen);//如果变了的话,我们begin后面minlen个范围的字符串获取然后返回就行了

  
  

    }

};

我们先创建hash1来记录我们t中字符出现的次数,创建变量kinds记录种类,
然后我们遍历t,获取我们的种类的数量,并且记录字符出现的频次

然后我们创建hash2来记录我们窗口中的每个字符出行的频次
创建变量minlen记录我们当前最小的子串长度,以及开始的指针begin

然后我们进入到滑动窗口的操作,我们先获取我们right指针的位置上的元素,并且在hash2中记录出现的次数
然后我们在进窗口后判断下当前进的这个字符是否是有效的字符,并且进行维护我们的count(字符种类)
是否是有效的字符,我们依靠这段代码hash2[in]==hash1[in],如果我们hash2中进的这个字符的次数等于我们hash1中字符的次数的话,那么我们就判定当前字符是有效的
如果有效的话,那么我们当前的count就++

进行判断,如果我们的count的大小等于我们的kinds的话,就是说明我们当前的窗口就是一个有效的窗口了
那么我们就进行结果的更新操作
记录我们当前区间的长度:right-left+1 并且记录我们当前的begin

然后我们再进行出窗口的操作,我们记录我们当前left位置的元素,然后从hash2中减少次数
并且我们在减少次数之前我们需要判断下我们这个出窗口的元素是否是有效的字符,

最后出了循环,我们就返回对应的结果了