当前位置: 首页 > news >正文

同向双指针——滑动窗口

目录

1 长度最小的子数组

2 无重复字符的最长子串

3 最大连续1的个数 III

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

5 水果成篮

6 找到字符串中所有字母异位词

7 串联所有单词的子串

8 最小覆盖子串


本文主要讲解滑动窗口算法的典型例题,滑动窗口从本质上可以理解为同向双指针,也就是left和right是同向移动且不会回退的双指针,用于维护一个性质相同元素或者符合某些特定条件的区间。

1 长度最小的子数组

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

题目解析:题目要求我们找出和大于等于target的子数组,返回所有符合条件的子数组的最小长度。

暴力解法:两层循环枚举出所有的子数组的和,时间复杂度O(N^2)

滑动窗口:题目给定所有的元素都是正数,对于一个区间[left,right],假设我们已经求出了他的和为sum,sum >= target,同时[left,righ-1]的和小于target ,那么此时以left为左端点的和大于等于target的子数组长度就是 right - left + 1;当我们继续去求以left+1为左端点的子数组的时候,可以继续从left+1一个一个元素枚举,直到和为target或者大于target。但是我们是不是可以继续利用上面已经求出来的 [left,right]的和sum,由于所有元素都是正数,那么[left+1,right-1]的和一定是小于[left,right-1]的和,也一定小于target,在找left+1的右端点时,可以直接从right开始枚举。

那么在本题:我们使用left和right来维护一段区间,使得这段区间内的元素和sum。

1、当sum < target 时,说明将right位置元素加入这个区间之后,和仍然小于target,那么[left,right]还需要继续往后添加元素来达到我们的要求。

2、当sum >= target时,说明将right位置元素加入进来之后,刚好是的[leftr,right]区间的元素和大于等于target,那么[left,right]区间就是以left为起点的和大于等于target 的最短子数组,我们需要记录一下。然后接着我们就需要去枚举left+1位置为起点的子数组,由于right可以继续使用,所以我们此时只需要更新窗口的左端点以及窗口的元素总和。

代码如下:

class Solution {
public:int minSubArrayLen(int target, vector<int>& nums) {int res = INT_MAX , n = nums.size();int left = 0 , right = -1 , sum = 0; //sum维护 [left,right] 的元素和while(right < n - 1){  //如果维护的是[left,right]的话,那么需要小于n-1,[left,right)则是n,但是操作顺序不一样//首先不断将元素入窗口,直到sum == target 或者 sum > targetwhile(right < n - 1 && sum < target){sum += nums[++right]; //将right+1位置元素加入进来,变成新的窗口}//窗口更新while(sum >= target){  //在这里能够更新出以right为结尾的最大的左端点,也就是最短子数组res = min(res , right - left + 1);sum -= nums[left++];  //left位置元素出窗口}}return res == INT_MAX ? 0 : res;}
};

2 无重复字符的最长子串

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

题目要求很简单,不需要额外解析了。

暴力解法:枚举每一个位置为起点的最长无重复字符的子串,时间复杂度为O(N^2)。

滑动窗口:我们维护 [left,right]区间是否相同字符,当我们将right加入窗口之后,发现有重复字符了,我们可以的值,[left,right-1]是没有重复字符的,那么[left,right-1]就是以left为起点的最长无重复字符的子串。而我们假设与 right位置相同的字符的位置为 index ,那么说明以 [left,index]范围为起点的最长无重复字符的最长字串的结尾都是right-1,而left自然是最长的那一个,所以我们其实只需要统计一个left的子串的长度就行。

而更新窗口的时候,我们需要直接将left更新到与right相同的位置index的下一个位置,然后继续计算该位置的最长无重复字符子串的右端点。

在统计是否重复的时候,我们可以数组来模拟哈希表,标记某个字符是否出现过,用 flag[i]表示ASCII为i的字符是否出现。

代码如下:

class Solution {
public:bool flag[128];int lengthOfLongestSubstring(string s) {int n = s.size();if(n == 0) return 0;int left = 0 , res = 1; //可以直接将res初始化为1for(int right = 0 ; right < n ; ++right){  //不管怎么样,右窗口是不断右移的,我么可以这样写//将right位置元素加入窗口//但是不一定能够直接加入,有可能已经有重复元素了if(!flag[s[right]]) flag[s[right]] = true;else{ //有重复,先统计一下left的最长子串res = max(res , right - left); //[left,right-1]//然后更新left直到将与right相同的元素出窗口for(;s[left] != s[right] ; left++) flag[s[left]] = false;//此时left位置与right相同,再右移一个left++;}}res = max(res , n - left);  //还需要统计一下right越界的时候,此时left一定是更新为了不会重复的左边界return res;}
};

3 最大连续1的个数 III

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

题目解析:给定一个只包含0和1的序列,我们最多可以将k个0翻转为1,求在这些操作之后,最长的连续1序列的长度。

暴力解法:枚举每一个起点在最多执行k次反转操作的情况下连续1的个数,时间复杂度为O(N^2)

滑动窗口:在枚举过程中其实我们没有必要真的对数组进行操作,只需要统计连续区间中0的个数,只要0的个数不超过k个,那么说明可以进行翻转操作使该区间全为1。

进窗口判断是0还是1,如果是1需要统计区间内的1的总个数。

当1的总个数大于k时,说明当前right是一个0,那么此时的left,它的最大连续1的区间就是[left,right-1],共有 right - left 个1。 假设left之后的第一个0的位置为index,那么起点在[left,index]范围内的时候,在最多执行k次操作的时候,右端点还是right-1,我们要的是最大的数量,所以只需要统计一个以left为起点的数量就行了。 在更新窗口left的时候,我们直接让left跳过left之后的第一个0。

代码如下:

class Solution {
public:int longestOnes(vector<int>& nums, int k) {int left = 0 , res = 0 , n = nums.size() , cnt0 = 0;for(int right = 0 ; right < n ; ++right){if(nums[right] == 0) cnt0++;if(cnt0 > k){res = max(res , right - left);for(;nums[left] != 0 ; ++left);++left,--cnt0;} }res = max(res , n - left);return res;}
};

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

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

题目解析:我们可以从数组的左边和右边进行元素删除,删除的时候从x中减去删除元素的值,求出刚好使x减到0时的最少操作次数。

暴力解法:递归,每一次都分两种情况讨论,删除最左边和最右边,记录所有可行方案的最小操作次数。时间复杂度O(2^N);

如果按照题目给的操作进行模拟的话,其实很复杂,可以换一种视角,就是如果能够在执行若干次操作之后使得x为0,不管怎么说,剩余部分一定是一个连续的子数组,而删除部分的和恰好为x,那么剩余子数组的和就是 数组元素总和sum - x。

那么我们只需要求出所有元素综合为sum-x的子数组,那么可以把他看成剩余部分,那么删除的部分就是操作的次数,假设剩余部分长度为 m ,数组总长度为n,那么执行的操作次数就是n-m;m越大,操作次数越少。

那么我们就将问题转化为了求出总和为恰好sum-x的子数组的最长长度。而算法原理就和第一题类似了。

代码如下:

class Solution {
public:int minOperations(vector<int>& nums, int x) {//转换为求和为sum-x的子数组的最长长度long long sum = 0 , n = nums.size() , sum1 = 0;for(auto x : nums) sum += x;long long goal = sum - x;if(goal < 0 )return -1;  //注意两种特殊情况if(goal == 0) return n;int res = 0 , left = 0 ;for(int right = 0 ; right < n ; ++right){sum1 += nums[right];while(sum1 >= goal){if(sum1 == goal) res = max(res , right - left + 1);sum1 -= nums[left++];}}return res == 0 ? -1 : n - res;}
};

5 水果成篮

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

解析题目:题目给定一个数组,fruits[i] 表示i位置上有一个种类为fruits[i]的水果,我们只能从某一个位置开始向右采摘水果,采摘水果的种类不能超过2,求出能够采摘的水果的最多数量。

暴力解法:枚举每一个起点,求出水果种类不超过2的最大的数量。

滑动窗口我们需要记录水果的种类,维护[left,right]窗口内水果种类不超过两种,如果超过2,那么说明right位置是一种新的水果,那么对于起点left来说,最多采摘到 right-1位置,水果数量为right-left。

当水果种类为3时,除了需要记录left的水果最大数之外,还需要更新窗口的左端点,我们需要将一种水果完全跳过,也就是让某一种水果的数量为0。那么在滑动窗口的时候我们除了要记录两种水果的种类之外,还需要记录两种水果的数量。当然我们不妨直接用一个哈希表来解决,通过判断hash.size()来得知水果的种类,当val为0时移除key,这比用四个变量来记录状态要简单得多。

代码如下:

class Solution {
public:int totalFruit(vector<int>& fruits) {int left = 0 , n = fruits.size() , res = 1;unordered_map<int,int> hash;for(int right = 0 ; right < n ; ++right){hash[fruits[right]]++;while(hash.size() > 2){hash[fruits[left]]--;if(hash[fruits[left]] == 0) hash.erase(fruits[left]);left++;}res = max(right - left + 1 , res); //确保了[left,right]区间内水果种类不超过2}return res;}
};

6 找到字符串中所有字母异位词

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

题目解析:给定字符串s和p,找出s中所有的p的字母字母易位词子串,所谓的字母易位词就是两个字符串字符相同,但是顺序可能不同。

暴力解法:枚举所有起点的长度为p.length的子串,判断是否为字母易位词,可以提取子串排序与排序之后的p判断是否相等。

本题的滑动窗口的优化其实主要优化在判断是否为字母易位词的时候,来避免排序的时间损耗。其实判断是否为字母易位词只需要判断两个字符串的字符数量是否完全相等。我们在窗口滑动的时候,其实窗口的长度是固定的。我们使用一个数组来记录p中每个字符的数量,一个数组来记录窗口内的每个字符的数量,长度都为26,然后每一次窗口移动都判断两个数组是否完全相等,如果完全相等,那么说明以left为起点的长度为len的子串是p的字母易位词。

但是我们每一次都需要进行长度为26的数组的遍历,时间损耗大,可以换一种思路。数组cnt1记录p的每个字符的数量,同时使用数组cnt2记录窗口的字符的数量,同时定义一个变量count用来记录有效字符数量,在字符ch入窗口的时候,如果该字符是一个p存在的字符,同时cnt1[ch] < cnt2[ch] ,那么说明这次入窗口的字符是一个有效的字符,那么我们++count。同时如果出窗口的时候,如果出窗口之后,cnt1[ch] < cnt2[ch] ,说明走掉了一个有效字符,此时--count ,那么每次窗口更新之后,我们判断一下count和p的长度是否相等,就能判断字母易位词。 在++count和--count的时候,除了要是p中有的字符之外,还需要前面所说的数量关系,这样就不用担心比如子串中的某个ch数量比p中的ch数量多了,在入窗口的还增加有效字符计数,可以避免这种情况,从而保证count==p.length的时候一定是字母易位词。

代码如下:

class Solution {int cnt1[26],cnt2[26];
public:vector<int> findAnagrams(string s, string p) {for(auto ch : p) cnt1[ch-'a']++;int left = 0 , count = 0 , n = s.size() , len = p.size();vector<int> res;for(int right = 0 ; right < n ; ++right){cnt2[s[right] - 'a']++;if(cnt2[s[right] - 'a'] <= cnt1[s[right] - 'a']) count++; //由于我们是先加加后判断的,所以用小于等于if(right - left + 1 > p.size()){//left位置出窗口cnt2[s[left]- 'a']--;if(cnt2[s[left] - 'a'] < cnt1[s[left] - 'a']) count--;++left;}if(count == len) res.push_back(left);}return res;}
};

7 串联所有单词的子串

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

题目解析:本题和上一题类似,只不过上一题要找的是字母和数量相同,顺序不同的子串,而这里要找的是单词和数量相同的子串。 

暴力解法:枚举所有的起点,找到长度为 words 的字符总个数的子串,判断是否满足条件。

其实本题的解题思路和上一个题几乎完全类似,我们可以把 words 中的一个一个字符串看成一个整体,统计每个字符串的个数。题目明确了每个word的长度len相同,那么我们在s中找的时候,也需要把s看成是一个一个长度为 len 的单次,而不是看成一个个的字母。

假设每一个word的长度都是len,那么我们可以这样看:

那么我们就可以使用滑动窗口,记录有效的子串数量,来判断以某一个位置为起点的长度为m*len的子串是否为满足条件的子串。

但是有一个问题就是,我们的第一个单词的起点并不一定是从0开始的,也可以是从 1,2,。。。len-1,所以我们需要枚举这 [0,n-1]种起点的情况。

由于在本题需要统计的是字符串的数量,那么我们可以使用哈希表来计数。

其他的细节都与上一题类似。

代码如下:

class Solution {
public:vector<int> findSubstring(string s, vector<string>& words) {unordered_map<string,int> hash1,hash2;int m = words.size() , n = s.size() , count = 0 , len = words[0].size();vector<int> res;for(auto& str:words) hash1[str]++;for(int start = 0 ; start < len ; ++start){count = 0;  //以start为第一个单词的开始位置hash2.clear();int left = start;for(int right = start ; right <= n - len ; right += len){ //注意right >== n - len 时后面已经没有一个长度为len的单词了string str = s.substr(right , len);hash2[str]++;if(hash1.count(str) && hash2[str] <= hash1[str]) count++;while((right - left) / len + 1 > m){  //(right - left)/len表示right之前的单词个数,还要算上right为起点的单词 string del = s.substr(left , len);hash2[del]--;if(hash1.count(del) && hash2[del] < hash1[del]) count--;left += len;}if(count == m) res.push_back(left);}}return res;}
};

8 最小覆盖子串

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

题目解析:我们需要在s中找出一个子串,这个子串需要包含t的所有字符,我们需要返回所有满足条件的子串的最小长度。

暴力解法:枚举所有的起点和终点的子串,判断是否满足条件。

对于一个区间[left,right]如果满足条件,那么当left不变时,right越大,我们的条件也不会不满足,所以我们需要找出left的最小能够满足条件的right,那么对于该left 而言,[right,n-1] 的右边界都是满足条件的,但是由于我们需要的是最小的子串长度,所以只需要找到这个right就行了,每一个left只需要记录 right - left + 1就行。 而right就是刚刚好满足条件的右边界。

在判断是否满足条件时,我们还是使用count来记录有效字符数量,快速判断当前窗口是否满足条件。

代码如下:

class Solution {
public:string minWindow(string s, string t) {int n = s.size() , m = t.size() , count = 0;unordered_map<char,int> hash1 , hash2;for(auto ch : t) hash1[ch]++;int left = 0 , len = INT_MAX , start = -1;for(int right = 0 ; right < n ; ++right){hash2[s[right]]++;if(hash1.count(s[right]) && hash2[s[right]] <= hash1[s[right]]) count++;if(count >= m){while(count >= m){  cout<<left<<"-"<<right<<endl;hash2[s[left]]--;if(hash1.count(s[left]) && hash2[s[left]] < hash1[s[left]]) count--;left++;}//出循环时表示,将left - 1 位置的字符移除之后刚好不满足条件,那么对于right而言,最大的左端点就是此时的left-1,那么[left-1,right]的长度就是 right - left + 2if(len > right - left + 2){   //这条语句只有在count >= m 时更新完窗口之后需要执行len = right - left + 2;start = left - 1;cout<<"start:"<<start<<"--len:"<<len<<endl;}}}if(start == -1) return "";return s.substr(start,len);}
};

总结

滑动窗口算法很适合用于解决类似于: 区间越大,越符合/不符合条件,找这种临界点的时候很方便。

http://www.dtcms.com/a/316357.html

相关文章:

  • 使用公众号的消息模板给关注用户发消息
  • UNet改进(30):SageAttention在UNet中的4-Bit量化实现详解
  • UOS20操作系统关闭NUMA和透明大页(UOS20+KunPeng920)
  • mq_timedreceive系统调用及示例
  • 工业设备远程监控的 “颠覆性突破”:边缘计算网关让千里之外如在眼前
  • 【图像算法 - 09】基于深度学习的烟雾检测:从算法原理到工程实现,完整实战指南
  • 16核32G硬件服务器租用需要多少钱
  • 【Redis初阶】------单线程模型
  • Next.js SSR 实战:构建高性能新闻网站
  • C++中的泛型算法(三)
  • 智慧城市SaaS平台|市容环卫管理系统
  • 【PHP】对数据库操作:获取数据表,导出数据结构,根据条件生成SQL语句,根据条件导出SQL文件
  • nordic通过j-link rtt viewer打印日志
  • Unknown initial character set index ‘255’,Kettle连接MySQL数据库常见错误及解决方案大全
  • 心念之球:在意识的天空下
  • Gemini CLI最近更新
  • GitLab:一站式 DevOps 平台的全方位解析
  • 笔记学习杂记
  • fastgpt本地运行起来的 服务配置
  • iptables 里INPUT、OUTPUT、FORWARD 三个链(Chain)详解
  • 编程算法:技术创新与业务增长的核心引擎
  • 如何在虚拟机(Linux)安装Qt5.15.2
  • STM32 外设驱动模块一:LED 模块
  • 第13届蓝桥杯Scratch_选拔赛_初级组_真题2021年10月23日
  • 基于MATLAB实现的频域模态参数识别方法
  • SpringAI:AI基本概念
  • 基于ARM+FPGA多通道超声信号采集与传输系统设计
  • PCIe Base Specification解析(六)
  • 五、逐波限流保护电路-硬件部分
  • 从零搭建Cloud Alibaba (下) Sentinel篇