滑动窗口--(上篇)

滑动窗口

在这里插入图片描述

长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 target 的长度最小的

子数组

[numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:

  • 1 <= target <= 109
  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105

题目解析
在这里插入图片描述

这里的关键点在于正整数,可以对代码进行优化,在单调性方面

算法原理
在这里插入图片描述

问题1:什么是“滑动窗口”?

“滑动窗口”是指同向双指针,像窗户一样移动的算法

问题2:怎么使用滑动窗口?

step 1:让左右指针指向同一块地方

step 2:进入窗口,左指针不动,右指针移动

step 3:判断是否需要更新结果或者出窗口,更新结果要先出窗口,在进行判断

问题3: 为什么滑动窗口的方法是正确的?

利用单调性,能够规避很多没有必要的枚举行为,我们需要先用暴力解法去了解,因为我们得到优化的解法,这道题题干说明了是正整数,所以太长了一定是大于目标值

问题4:时间复杂度怎么样?

看下面的代码两层循环可能会以为是O(N2),但是实际并不是,是O(N)

代码如下:

class Solution {
public:
    int minSubArrayLen(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;
    }
};

更详细来说,

解法⼀(暴⼒求解)(会超时):

算法思路:

「从前往后」枚举数组中的任意⼀个元素,把它当成起始位置。然后从这个「起始位置」开始,然 后寻找⼀段最短的区间,使得这段区间的和「⼤于等于」⽬标值。

将所有元素作为起始位置所得的结果中,找到「最⼩值」即可

算法代码:

class Solution {
public:
 int minSubArrayLen(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 )往后的和,势必会有⼤量重复 的计算(因为我们在求第⼀段区间的时候,已经算出很多元素的和了,这些和是可以在计算 下次区间和的时候⽤上的)。

▪ 此时, rigth1 的作⽤就体现出来了,我们只需将 left1 这个值从 sum 中剔除。从 right1 这个元素开始,往后找满⾜ left2 元素的区间(此时 right1 也有可能是满 ⾜的,因为 left1 可能很⼩。 sum 剔除掉 left1 之后,依旧满⾜⼤于等于 target )。这样我们就能省掉⼤量重复的计算。

▪ 这样我们不仅能解决问题,⽽且效率也会⼤⼤提升.

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

无重复字符的最长字串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长

子串

的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

在这里插入图片描述

在这里插入图片描述

代码如下:

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]]++;//进入窗口
            while(hash[s[right]]>1)//判断
                hash[s[left++]]--;//出窗口
                ret=max(ret,right-left+1);//更新结果
                right++;

        }
        return ret;
    }
};

解法⼀(暴⼒求解)(不会超时,可以通过):

算法思路:

枚举「从每⼀个位置」开始往后,⽆重复字符的⼦串可以到达什么位置。找出其中⻓度最⼤的即 可。

在往后寻找⽆重复⼦串能到达的位置时,可以利⽤「哈希表」统计出字符出现的频次,来判断什么 时候⼦串出现了重复元素。

算法代码:

class Solution {
public:
 int lengthOfLongestSubstring(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 ,说明当前窗⼝没有重复元素,可以直接更新结果

最大连续1的个数

给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k0 ,则返回 数组中连续 1 的最大个数

示例 1:

输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。

示例 2:

输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。

提示:

  • 1 <= nums.length <= 105
  • nums[i] 不是 0 就是 1
  • 0 <= k <= nums.length
    在这里插入图片描述

在这里插入图片描述

解法(滑动窗⼝):

算法思路:
不要去想怎么翻转,不要把问题想的很复杂,这道题的结果⽆⾮就是⼀段连续的 1 中间塞了 k 个 0 嘛。

因此,我们可以把问题转化成:求数组中⼀段最⻓的连续区间,要求这段区间内 0 的个数不超 过 k 个。

既然是连续区间,可以考虑使⽤「滑动窗⼝」来解决问题。

算法流程:

a. 初始化⼀个⼤⼩为 2 的数组就可以当做哈希表 hash 了;初始化⼀些变量 left = 0 , right = 0 , ret = 0 ;

b. 当 right ⼩于数组⼤⼩的时候,⼀直下列循环:

i. 让当前元素进⼊窗⼝,顺便统计到哈希表中;

ii. 检查 0 的个数是否超标:

• 如果超标,依次让左侧元素滑出窗⼝,顺便更新哈希表的值,直到 0 的个数恢复正 常;

iii. 程序到这⾥,说明窗⼝内元素是符合要求的,更新结果;

iv. right++ ,处理下⼀个元素;

c. 循环结束后, ret 存的就是最终结果。

代码如下:

class Solution {
public:
    int longestOnes(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;
    }
};
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值