LeetCode从零开始之滑动窗口篇

LeetCode 904 水果成篮问题
原题截图
初读题时觉得摸不着头脑,没有理解本题考察内容,但在阅读过示例后逐渐理解此题考察内容
本题考查数组中满足只含两种元素的子数组最大长度
结合LeetCode 76最小覆盖子串的思路,尝试使用滑动窗口解决问题。

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        //左右边界
        int i = 0,j = 0;
        //可以采摘树的最大值
        int ans = 1;
        //树的种类
        int nums = 1;
        //边界值
        int length = fruits.size();
        //当前树的种类 设置为0是因为没有种类为0,避免重复
        int temp = 0,temp1 = fruits[0];
        //指针j的临时值
        int tempj = 0;
        //大循环
        while(i <= j && j < length-1){
            //条件起始
            if(nums == 1){
               //右边界右移一格
                j++;
                //比较当前树种类与上一棵树种类
                if(fruits[j] != fruits[j-1]){
                    nums++;
                    //保存右边界j的当前位置
                    tempj = j;
                    //保存该不同树的种类
                    temp = fruits[j]; 
                }
                //判断当前长度是否超过原长度
                if(j-i+1 > ans){
                    ans = j-i+1;
                }
                continue;
            }
            if(nums == 2){
              //右移边界
                j++;
                //比较当前树的种类
                if(fruits[j] != fruits[j-1]){
                	//前树的种类与之前两种树种类都不相同
                    if(fruits[j] == temp || fruits[j] == temp1){
                        //保存当前右边界 j 的位置
                        tempj = j;
                    }
                    //当前树的种类与之前两种树种类的其中之一相同
                    if(fruits[j] != temp && fruits[j] != temp1){
                        nums++;
                    }
                }
                //判断当前长度是否超过原长度,更新长度
                if(nums != 3){
                   if(j-i+1 > ans) {
                    ans = j-i+1;
                }
                }
                continue;
            }
            if(nums == 3){
           		//退回种类为2的情况
                nums--;
                //更新左边界
                i = tempj;
                //更新右边界
                tempj = j;
                //保存此时两棵树的种类
                temp = fruits[i];
                temp1 = fruits[j];
                continue;
            }
        }
        return ans;
        }
    
};

第一部分 nums=1(树种类为1)

先将右边界右移一格,比较当前树与上一棵树种类
此时有两种情况:

1. 与上一颗种类不同

则种类数+1,此时我们要保存当前右边界 j 的位置, 原因我们回头再讲。
并保存该不同树的种类。

2.与上一颗种类相同

可以继续采摘,continue即可。

3.最后判断当前长度是否超过原长度,更新长度
if(nums == 1){
                //右边界右移一格
                j++;
                //比较当前树种类与上一棵树种类
                if(fruits[j] != fruits[j-1]){
                    nums++;
                    //保存右边界j的当前位置
                    tempj = j;
                    //保存该不同树的种类
                    temp = fruits[j]; 
                }
                //判断当前长度是否超过原长度
                if(j-i+1 > ans){
                    ans = j-i+1;
                }
                continue;
            }

第二部分 nums=2(树种类为2)

依旧先右移边界,然后进行比较当前树的种类,此时依然有两种情况

1.种类不同

此时依然有两种可能:

1.1 当前树的种类与之前两种树种类都不相同

fruits[j] != temp && fruits[j] != temp1
那很简单 只需要将种类+1即可 交给nums=3的部分处理。

1.2 当前树的种类与之前两种树种类的其中之一相同

在这种情况下,我们依然可以继续摘水果,但注意 此时我们要保存当前右边界 j 的位置

2.种类相同

此种情况较为简单,依然可以继续摘水果。无需处理

3.最后判断当前长度是否超过原长度,更新长度

注意要进行树种类数判断,因为此时有树种类为3的情况,注意排除。

if(nums == 2){
				//右移边界
                j++;
                //比较当前树的种类
                if(fruits[j] != fruits[j-1]){
                	//前树的种类与之前两种树种类都不相同
                    if(fruits[j] == temp || fruits[j] == temp1){
                        //保存当前右边界 j 的位置
                        tempj = j;
                    }
                    //当前树的种类与之前两种树种类的其中之一相同
                    if(fruits[j] != temp && fruits[j] != temp1){
                        nums++;
                    }
                }
                //判断当前长度是否超过原长度,更新长度
                if(nums != 3){
                   if(j-i+1 > ans) {
                    ans = j-i+1;
                }
                }
                continue;
            }

第三部分 nums=3(树的种类为3)

种类为3 说明当前第一次摘水果的尝试结束,需要进行第二次采摘尝试。
但起始位置不能回到最初,那么我们应该从哪里开始呢?应当从最近的两种树的种类开始
保存最后两种树的种类与位置,此时变量 tempj 就派上用场了,原来右边界 j 的位置变为当前左边界 i 的位置,此时 j 的位置变为新的右边界位置。
最后再保存此时两棵树的种类,使其返回种类为2的情况即可。

if(nums == 3){
                //退回种类为2的情况
                nums--;
                //更新左边界
                i = tempj;
                //更新右边界
                tempj = j;
                //保存此时两棵树的种类
                temp = fruits[i];
                temp1 = fruits[j];
                continue;
            }

到此为止三种情况全部分析完毕,当左边界移动到右边界或右边界抵达尽头时,循环结束。
注意大循环条件为 j<length-1,防止 j++ 超过下标长度。

文章至此遗留的问题“为何保存右边界的位置” 这个问题的答案便变得清晰了,这其实就是移动窗口的核心,左右边界的移动问题。
回顾本题思路,左边界 i 起始为数组起点,右边界 j 起始与左边界相同,逐渐向右移动,期间进行条件判断与处理。当满足条件时,持续更新结果,右边界不断右移。直至条件破坏退出第一次迭代。
此时面临左边界更新,根据本题条件,左边界应更新为原右边界位置。
此题为寻找最大窗口,接下来一道题为最小窗口应用。

在这里插入图片描述
首先想到统计t字符串里的各字符的出现频数。因为题解要求子字符串数量必须不少于t中字符数量,因此在寻找窗口时需将频数纳入判断条件。
此题我们借助unordered_map即无序map容器帮助实现。

class Solution {
public:
    string minWindow(string s, string t) {
    	//定义字符串t统计容器与窗口容器
        unordered_map<char,int> need,window;
        //统计t字符串频数 并写入容器
        for(char c : t){
            need[c]++;
        }
        //定义窗口左右边界
        int left = 0,right = 0;
        //定义符合最小容量个数
        int valid = 0;
        //定义最小字符串起始位置,字符串长度
        int start = 0,len = INT_MAX;
        //生成符合条件窗口
        while(right < s.size()){
            //提取右边界元素
            char c = s[right];
            //右边界右移
            right++;
            //元素判断
            if(need.count(c)){
                //t中包含则window中对应元素容量+1
                window[c]++;
                //如果该元素容量满足t中要求,则valid+1
                if(window[c] == need[c]){
                    valid++;
                }
            }
            //右边界持续右移直到所有元素容量均符合要求
            while(valid == need.size()){
            	//获取当前子字符串长度并比较,小于则更新
                if(right - left < len ){
                    len = right-left;
                    //同时定义当前最小窗口左边界位置
                    start = left;
                }
               	//提取当前左边界元素
                char d=s[left];
                //左边界向右收缩窗口
                left++;
                //判断删去当前左边界元素后是否满足条件
                if(need.count(d)){
                	//不满足则更新valid值,以跳出该while循环
                    if(window[d]==need[d]){
                        valid--;
                    }
                    //更新该元素容量
                    window[d]--;
                }
            }
        }
        return len == INT_MAX ? "" : s.substr(start, len);
    }
};

纵观本题思路,先从左至右逐渐扩大窗口直至满足条件,而后慢慢从左边界向右收缩并判断条件,如条件破坏,则右移右边界直至重新满足条件,而后左边界重新收缩…这样不断循环直至边界破坏。
对比水果问题我们发现,最小窗口问题是左边界不断右移更新结果,最大窗口时右边界不断右移更新结果,因此我们可以总结窗口移动问题核心为 :

一个边界不动,另一边界不断移动以解决问题

大体流程为:
流程
另外我们注意到滑动窗口问题更倾向于使用 while循环解决问题,因为相对于 for循环,while循环可以很好的控制复杂度,建立起良好的边界值移动法则,便可以避免双重for循环嵌套导致的复杂度暴涨至O(n2)的问题。
关于滑动窗口的总结就暂时到这里,后续有更多的题目或者心得会持续更新。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值