算法篇-----滑动窗口

1.概念

所谓的滑动窗口,就是我们之前的双指针的一个扩展应用,在上一章中,我们的双指针是相向而行的,而这里的双指针是同向而行的,由于其移动过程中像一个窗口一样来回滑动,时大时小,而且还会来回动,因此我们给他起了一个名字:滑动窗口

2.使用场景

求给定数组的子数组时可能会用到,具体可以结合下文的示例来感受

3.例题

例1:长度最小的子数组

209. 长度最小的子数组 - 力扣(LeetCode)

这个题要求我们求找出该数组中满足其总和大于等于target的长度最小的 **子数组,**符合滑动窗口的使用场景,下面我将具体阐述解题方法。

解题思路:

由于数组里面全是正数,并且是求和,因此我们可以利用一下单调性(肯定不会出现越加越小的情况),初始状态下我们设置两个指针left和right,并都指向下表为0的位置,并且假设left是左边框,right是右边框,设计一个求和的变量,right每次走的时候都自动计算一下当前的总和,并与target比较,如果小于target就继续++,如果大于等于target就先计算一下长度,并且长度要和之前的长度值进行比较,取最小的,由于我们要去最小的length,所以我们可以让left++,再求一下和(这里可以直接让sum-=arr[left++])就好,之后不断重复上述过程,直到找到最短的length位置。

参考代码:

复制代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) 
    {
        int n=nums.size();
        int left=0,right=0;
        int sum=0;
        int 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;
    }
};

例2:无重复字符的最长字串

3. 无重复字符的最长子串 - 力扣(LeetCode)

这道题要求我们寻找一个没有重复字符的字串,而且字串是要连续的,那么我们还是两种方法来解决这道题

解题思路:

方法一)暴力破解

将每个符合的子字符串都写出来,分别求长度

方法二)滑动窗口

我们假设有这样一个数组[ d e a b c a b c],先设置两个指针,left和right,之后让right往后走(进窗口),与此同时,我们可以借用一下哈希表,由于哈希的本质就是映射,而题里面都是常见字符,为此我们可以用数组模拟哈希表,定义个hash[128],哪个字符进去了,就在其映射的哈希表的位置上做上标记,完成这一步后,让right一直往后走,直到right所对应的字符对应的映射在之前的哈希表中出现过为止,如上例中,left=0,right走到第二个a处停止,此后,由于我们的right已经给我们"趟过雷"了,此时将left在++到e的位置,right在从e开始走就多此一举了,可以让left直接++到第一个a后面的字符,也就是b,然后right继续往前走就好,不断重复上述过程,直到我们找到最优解为止。

参考代码:

复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) 
    {
        int hash[128]={0};
        int left=0,right=0,n=s.size();
        int ret=0;
        while(right<n)
        {
            hash[s[right]]++;    //将right位置的数映射到哈希表上,是我们的入窗口的操作
            while(hash[s[right]]>1)
            {
                //这个位置之前被映射过了,证明当前的是重复的字符
                //出窗口,找到没有映射的位置
                hash[s[left]]--;    //我们的left准备向右移动了,对应的映射也需要变化了
                left++;             
                //上例中,当left++到第一个b的时候,原来第一个a的映射就没了,我们也就跳出循环了
            }
            ret=max(ret,right-left+1);        //更新结果
            right++;                         //让下一个元素进入窗口
        }
        return ret;
    }
};

例3:翻转数字(最大连续1)

1004. 最大连续1的个数 III - 力扣(LeetCode)

这道题题意很简单,要求我们可以最多反转K个0,之后找出最长的1的子字符串

解题思路:

这道题有一些小坑,如果直接考虑反转,那可能有点麻烦了,我们不妨转换一下题意,也就是说找一个字符串,其0的个数不超过K个就可以。这道题还是有两种解题思路

法一)暴力枚举+计数器

分别举出每一段子字符串,并且0的个数要符合要求,之后找出最长的子字符串

法二)滑动窗口+计数器

我们可以仿照上一个题,定义两个指针Left和right,以及一个计数器count,让right不断往右走,遇到零计数器就++,否则不触发任何反应,继续往右走(进窗口),当计数器达到k的时候(判断条件),让left也往左走(出窗口),1则不触发任何反应,继续往右走,0则计数器--,重复上述操作,直至right走到头并且符合判断条件

参考代码:

复制代码
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) 
    {
        int ret=0,left=0,count=0,right=0;
        for(right=0;right<nums.size();right++)
        {
            //进窗口
            if(nums[right]==0)
            {
                count++;
            }
            while(count>k)
            {
                if(nums[left]==0)
                {
                    count--;
                }
                left++;
            }
            ret=max(ret,right-left+1);
        }
        return ret;
    }
};

例4:将 x 减到 0 的最小操作数

1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)

这道题给定我们一个数,并要求我们在数组内依次选一个数来减去x,直到x为0,如果存在则返回最小操作数,如果不存在则返回-1,这道题看起来貌似有一些困难,那我们应该怎么做呢?

解题思路:

想必大家都学过高数,在数学中,我们了解到正难则反,此题同理,我们只需要找到中间最长的子数组,并且其和target等于该数组元素总和减去x即可,这样,我们便将题迎刃而解了

我们不妨设两个指针left和right,以及和total,之后开始进窗口操作,即将Total增加,之后再判断total与target的关系即可,如果大于,那么便出窗口,如果等于就更新结果,最后找最长的子数组就Ok,最后返回最小操作数

代码示例:

复制代码
class Solution {
public:
    int minOperations(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,total=0;right<nums.size();right++)
        {
            total+=nums[right];
            while(total>target)
            {
                total-=nums[left++];
            }
            if(total==target)
            {
                ret=max(ret,right-left+1);
            }
        }
        if(ret==-1)
        {
            return ret;
        }
        else
        {
            return nums.size()-ret;
        }
    }
};

例4:水果成篮

904. 水果成篮 - 力扣(LeetCode)

这道题像是一个小的阅读理解一样,我们可以简单分析一下,并借助注释简化一下题中的表达思想,最终,我们可能将题目简化为在一个数组中找长度最长的子数组,并且里面只能由两种数(水果种类),由此我们便可以开始解决这道题了

解题思路:

由于所有的解题思路都是由暴力解法演变而来的,所以我们在这里先讲解一下暴力解法

方法一)暴力破解+哈希表

这种方法思路很简单,就是从头开始,挨个列举出来子数组的长度,找到最大值后返回,那如何判断此时的子数组达到最大值呢?我们这里可以借助哈希表,利用映射关系,当哈希表的长度等于2时,便达到了最大长度。如下图所示

方法二)滑动窗口+哈希表

这种方法是基于暴力解法转换而来的,当水果种类Kinds==2时,我们必然要移动左指针,这时就会出现两种情况,一种是kinds减小,另一种是kinds不变,原因很简单不解释了,对于第一种情况,右指针不能动,对于第二种情况,右指针要右移,为此我们找到了这道题的破解点,滑动窗口。具体解法就是设置两个指针,还是进窗口出窗口判断等操作,对于进窗口操作,直接借助哈希表的映射,让hash[f[right]]++,出窗口就是hash[f[left]]--,但是有一点值得注意的是我们还要记录一下每种水果已经有了多少个,,并且最好还要有一个计数的,所以我们用hash<int,int>,也可以借助unordered_map来实现,有一个小细节就是我们希望减到0时就自动把这个水果给删掉,以免影响水果种类的判断,判断条件就不多说了,就是hash.size()>2,我们就出窗口

代码示例:

复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) 
    {
        unordered_map<int,int> hash;
        int ret=0;
        for(int left=0,right=0;right<fruits.size();right++)
        {
            hash[fruits[right]]++;     //进窗口
            while(hash.size()>2)
            {
                //出窗口
                hash[fruits[left]]--;
                if(hash[fruits[left]]==0)
                {
                    hash.erase(fruits[left]);
                }
                left++;
            }
            ret=max(ret,right-left+1);
        }
        return ret;
    }
};

这样或许时间复杂度有点高,我们给改一下,由于题目给定了我们水果的个数,所以我们可以直接利用数组来模拟哈希表,只不过要再增加一个变量Kinds,相当于用空间换时间了。

改造后代码:

复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) 
    {
        int hash[100001]={0};
        int ret=0;
        for(int left=0,right=0,kinds=0;right<fruits.size();right++)
        {
            //这里维护的是水果种类,不是个数,不能写成进窗口就单纯的kinds++,要看看之前有没有这种 
              水果
            if(hash[fruits[right]]==0) kinds++;    //维护水果种类
            hash[fruits[right]]++;     //进窗口
            while(kinds>2)
            {
                //出窗口
                hash[fruits[left]]--;
                if(hash[fruits[left]]==0)
                {
                    kinds--;
                }
                left++;
            }
            ret=max(ret,right-left+1);
        }
        return ret;
    }
};

改造完成!

例5:找字母异位词

438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

这道题要求我们在一个字符串中,找一个子字符串,要满足字母组成上可以与给定字符串不同,但必须是由这几个字符所构成,并返回第一个字符的索引。

解题思路:

题目既然要求我们找与给定字符串异位的子字符串,那么我的第一想法是利用哈希表,统计出给定字符串中每个字符出现的次数。之后我们在利用哈希表,并通过滑动窗口的方法,统计窗口里面的每一个字符出现的个数,这里为什么会联想到滑动窗口呢?原因就是因为我们这两个指针之间的距离是固定的,就是给定字符串的长度,right++必然就要left++,更像滑动窗口了,好我们回到问题本身,我们在统计窗口里面的每一个字符出现的个数之后,可以再设置一个计数器count,目的是记录我们有效的字符,举个例子,比如给定字符串由3个字符a,而我现在窗口里面又滑进来一个a,现在假设我们的总共在窗口里面又3个,那新进来的a就是有效字符,而换句话讲,如果我现在窗口里面已经6个a了,那新来的这个a就没啥用了,count就不需要++,如此一来,只需要我们的count与给定字符串的长度相等,那就证明符合异位,这就是我们需要的子字符串,出窗口同理,也要维护一下count

代码实现:

复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) 
    {
        vector<int> ret;
        int hash1[26]={0};      //统计字符串P中每个字符出现的个数
        for(auto e:p)
        {
            hash1[e-'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++;    //进窗口+维护count
            if(right-left+1>m)//判断     要保证子字符串的长度
            {
                char out=s[left++];
                if(hash2[out-'a']--<=hash1[out-'a']) count--;  //出窗口+维护count
            }
            //更新结果
            if(count==m) ret.push_back(left);
        }
        return ret;
    }
};

例6:串联所有单词的子串

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

这道题是我们上一道题的延申,我们可以将题目的意思翻译一下,就是说在大字符串里面找小字符串,并且小字符串与word字符串是可以异位的

解题思路:

由于给定字符串中每个单词的长度是一样的,所以在这里我们可以将长字符串分割成几个短字符串。在上述事例中,w有两个三个字母的单词组成。那么我们便可以将大字符串三个字符为一组分割成若干份,从头开始画,也许有人会问,万一是从第二个字符开始它才是符合的,那应该怎么办?这里边要借助我们的滑动窗口来解决了。我们可以让起始滑动的位置从左向右依次移动,当移动到第二个起始位置之时,及上述事例的字母f,此时我们便可以停止移动,因为再移动也是重复的没有意义,那我们怎么确定f的位置呢?这里边又要借用w中每个单词的长度是相等的,可以求出单词的长度,这样就可以确定出f的位置。之后再利用哈希映射等方法可以破解此题,破解方法与上一题相似。

参考代码:

复制代码
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) 
    {
        vector<int> ret;
        unordered_map<string,int> hash1;     //保存words里面所有单词的频次
        for(auto& e:words)  hash1[e]++;
        int len=words[0].size();        //求出一个单词有几个字符
        int 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;
    }
};

例7:最小覆盖字串

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

这个题要求我们在一个字符串中找的一个最小字串,并且这个最小字串要包含给定的另一个字串,那么我们应该怎么做呢?

解题思路:

法一)暴力破解+哈希表

这道题基本思想和前面的差不多,我们第一种想法就是暴力破解,我们可以先遍历字符串2,探明要寻找的字母类型和个数,并且可以将其映射在哈希表中,之后我们再一一例举字符串1的所有符合题目条件的子字符串,并找出最小子字符串,完成本题

法二)滑动窗口+哈希表

我们还是遍历字符串2,并将每个字符都入hash2

之后可以定义两个指针left和right,之后让right往后走,每次走之前都让right所指元素入哈希表,当right入的是有效字符(即是hash2里面的字符)时,可以check(hash2,hash1),符合条件就更新结果,之后让left++,可能还会有两种结果,一种是符合要求,那我们的right就不用动,另一种是不符合要求,那我们的right就要++了,不断重复上述操作,直至循环结束!

优化:

由于我们的每一次check的花销都会很大,因为需要遍历一下hash1和hash2,而且指针只要碰到一个有效字符就要check一下,那倘如字符串2里面有一万个a,我们的字符串里面有两万个a,那我们的编译器岂不是不用干别的了?因此,可以将上述代码给出优化!

优化的方法也很简单,我们可以定义一个变量count,用于标记有效字符的种类,并且在进窗口和出窗口时对其进行维护,具体来说就是在进窗口之后,当hash2[in]==hash[1]时count++,那为什么是=而不是>呢?原因就是=的时候就已经符合条件了,如果>的时候再去统计就会导致这个字符重复统计,并且也没有必要,然后在出窗口之前,如果将要出去的字符符合hash2[out]==hahs1[out],那么我们的count就要--,之后我们的判断条件就可以变为count==hash1.size()

参考代码:

复制代码
class Solution {
public:
    string minWindow(string s, string t) 
    {
        int hash1[128]={0};
        int kinds=0;     //统计有效字符有多少种
        int hash2[128]={0};
        for(auto e:t)
        {
            if(hash1[e]==0)
            {
                kinds++;
            }
            hash1[e]++;
        }
        int minlen=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<minlen)
                {
                    minlen=right-left+1;
                    begin=left;
                }
                char out=s[left];
                left++;
                if(hash2[out]==hash1[out])
                {
                    count--;
                }
                hash2[out]--;
            }
        }
        if(begin==-1)
        {
            return "";
        }
        else
        {
            return s.substr(begin,minlen);
        }
    }
};

至此,滑动窗口篇章结束!!!

接下来的文章,将会为大家讲解二分查找的算法!

相关推荐
yuanManGan12 分钟前
领略算法真谛: 多源bfs
算法·宽度优先
?!7141 小时前
数据结构之哈夫曼树
c语言·数据结构·c++·算法
xiaoli23271 小时前
机器学习朴素贝叶斯算法
人工智能·算法·机器学习
xin007hoyo1 小时前
算法笔记.约数个数
数据结构·笔记·算法
yy鹈鹕灌顶1 小时前
十大排序算法全面解析(Java实现)及优化策略
java·算法·排序算法
sunshineine1 小时前
Linux系统安装PaddleDetection
linux·运维·服务器·人工智能·算法
n33(NK)1 小时前
【算法基础】三指针排序算法 - JAVA
java·算法·排序算法
图灵科竞社资讯组1 小时前
数据结构--树状数组
数据结构
.格子衫.2 小时前
014枚举之指针尺取——算法备赛
java·c++·算法
CodeWithMe2 小时前
【中间件】bthread_数据结构_学习笔记
数据结构·学习·中间件