滑动窗口
滑动窗口概念
滑动窗口是一种在数组或字符串上执行数据处理的算法设计技巧。该技巧通常用于解决涉及连续子数组或子字符串的问题。滑动窗口的基本思想是通过维护一个可变大小的窗口,滑动这个窗口来遍历整个数据集,从而解决特定问题。
基本思路
1.初始化窗口: 定义一个窗口的起始位置,通常使用指针或索引表示。初始化时,确定窗口的大小和初始位置。
2.滑动窗口: 使用循环遍历整个数据集,移动窗口的结束位置。在每一步,更新窗口的状态,以便解决问题。
3.处理窗口内的数据: 在每个窗口的位置,进行特定的操作,例如计算窗口内的和、平均值,找到窗口内的最大值或最小值等。
4.根据问题调整窗口大小: 根据问题的需求,可能需要调整窗口的大小。这可能涉及到窗口的扩展或缩小,以适应不同的情况。
5.记录结果: 在滑动的过程中,通常会记录每个窗口位置的结果。这些结果可以用于最终解决问题的输出或进一步的分析。
例题
1.长度最小的子数组
题目描述
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
按照基本思路我们需要定义两个指针来表示窗口的起始位置与终止位置,即为 left 与 right.
sum用来处理窗口内的数据加和是否满足目标
此时我们开始遍历数据集,移动right来移动窗口的结束位置.此时窗口即为right与left之间的部分.
窗口内数据加和sum=2
由于窗口内的数据加和sum仍然不满足 大于等于target
继续移动窗口的结束位置
此时窗口内元素数据加和sum=8大于目标值target,次数满足条件,所以min赋值为当前满足条件的子数组长度4.
找到了一组满足条件的子数组,显然我们就不能在继续移动窗口的结束位置了,要不然后续肯定是满足的**
下面我们就需要移动窗口的起始位置来进行寻找最小数组(第一次找到的满足条件的子数组是第一个元素开始的,我们需要开始从第二个开始寻找。当然我们可以用for循环来遍历,但这样时间复杂度大)
移动窗口的起始位置,此时窗口内的数据加和sum=6,不满足条件,下一步我们就需要改变窗口的大小
再将窗口结束位置移动一位
此时窗口大小就在此满足条件,但此时窗口长度仍为4,所以min的值不进行更新.
同理移动窗口的起始位置
满足条件,并且此时的窗口大小为3.
将最小数组长度更新为3
再次移动窗口的起始位置来寻找满足条件的窗口.
不满足后继续移动right
然后继续处理当前窗口内的数据,满足则移动起始位置,如果不满组则退出循环。
移动后发现刚好满足,此时窗口大小为2,很显然针对此用例已经找出了所有满足条件的子数组。
最后将最小的子数组跟新为2,然后跳出循环.
该方法通过滑动窗口,算法能够利用前一窗口的计算结果,避免不必要的重复计算。这在一些问题中能够提高算法的效率。
且窗口的大小通常可以根据问题的要求进行动态调整,使得算法更加灵活,能够适应不同的数据特性。
int minSubArrayLen(int target, int* nums, int numsSize)
{
int left = 0;//窗口的起始位置
int right = 0;//窗口的结束位置
int sum = 0;//处理窗口内数据的加和值
int min = numsSize + 1;//返回的最小窗口的值,可以为定义是可以为任意值,但不得为0,否则,会使min的始终无法更新.
for(right = 0; right < numsSize; ++right)//窗口结束位置最远为数据尾部
{
sum+=nums[right];//处理当前窗口内的数据和
while(sum >= target)//判断窗口是否满足
{
int sub=right-left+1;//满足目标的窗口长度,与图示不同的是数组的下标是从0开始的。
min=sub<min?sub:min;//更新min的值
sum-=nums[left++];//移动窗口的起始位置,并将移动前的值减去.
}
}
return min==numsSize+1?0:min;//最后返回最小值.
}
移动起始位置还是终止位置:可以理解为满足则移动起始位置,不满足则移动结束位置.
2 水果成篮
题目描述
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 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 = [3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘 [1,2,1,1,2] 这五棵树。
int totalFruit(int* fruits, int fruitsSize)
{
int right = 0;//第一个遇见的值的下标,然后往后循环
int left = 0;//用来记录一次寻找后减去第一个类型的下标.例如112223312第一次寻找后left的下标更新为2.
int i = 0;//记录第一个类型,相当于某一个篮子可以装水果
int j = 0;//第二个类型.另外一个篮子装的水果
int count = 0;//每一次循环的可以摘到的苹果数量.
int maxCount = 0;//记录最多可以��到的��果数量.
while (right < fruitsSize)
{
i = fruits[right];//i赋值为第一个类型
int k = right + 1;//定义一个变量来确定第二个类型的下标.
count = 0;//初始化count.
while (k < fruitsSize && fruits[k] == i)
{
k++;//如果当前的下标等于第二个类型的下标,则count++
}
if (k < fruitsSize)
{
j = fruits[k];//将第二个类型赋值给第二个类型.
}
left=k;//记录第二个类型的下标,用于下一次循环起始循环的位置.
while (right < fruitsSize && (fruits[right] == i || fruits[right] == j))
{
count++;//开始统计数量
right++;//向后移动用来遍历数组
}
right=left;//记录第二个类型的下标,用于下一次循环起始循环的位置.
if (count > maxCount)
{
maxCount = count;//赋值.
}
}
return maxCount;
}
- 第一个while循环来限制窗口的结束位置不得超过数据尾部,也可用for循环来限制
- 第二个while循环来寻找另外一个水果类型
- 第三个for循环来进行遍历数组来寻找满足条件的窗口.
- 本题的思路为找到一组数据满足条件之后就将下次窗口的起始位置定为第二种水果类型的起始位置,然后从当前位置在开始向后寻找满足条件的窗口.
严格滑动窗口详解
3 最小覆盖字串
题目
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
解释:最小覆盖子串 “BANC” 包含来自字符串 t 的 ‘A’、‘B’ 和 ‘C’。
我们拿这一组例子来给大家理一下基本思路.
left 和 right 为窗口的起始与结束位置
不满足条件则移动窗口的结束位置,所以right一直右移到当前位置满足条件
left移动到当前位置则不满足条件则开始移动结束位置.
同理列出满足条件的窗口位置。
思路理解了,窗口内的满足条件即为窗口内包含目标字符串的全部字符.
此题还有一个难点是如何表示窗口内此时含有目标字符串的全部字符.
我们可以用数组来维护窗口内的数据,例如先统计一遍目标字符串的字符以及数量,用整形数组存储个数,这样在遍历s数组是遇到想对应的字符则数目减1,当整形数组中没有元素的值大于0,则代表此时的窗口满足条件.
char* minWindow(char* s, char* t)
{
int map[256] = { 0 }; //用 map[] 统计字符串 t 中各字母的个数
int current = 0;
int minlen = strlen(s)+1;
int start = 0;
int sLen = strlen(s), tLen = strlen(t); //用 tLen 统计还未找到的字符串 t 中字母的个数
for (int i = 0; i < tLen; i++)
{ //统计字符串 t 中各字母的个数
map[t[i]]++;
}
for (int left = 0, right = 0; right < sLen; right++)
{
if (map[s[right]]-- > 0)
{
tLen--; //是对应字母,则代表找到l相应的一个字母
}
while (tLen == 0)
{
//窗口中包含了t中的全部元素
current = right - left + 1;
if (current < minlen)
{
//将窗口起点进行更新,并比较最小长度
start = left;
minlen = current;
}
if (++map[s[left]] > 0) { //统计窗口中的字母个数,左边移动到不满足全部元素出现的时候退出循环.
tLen++;//所以当左边的元素加一大于0,,就说明左边的元素不满足全部元素出现
}
left++; //左边移动1位
}
}
if (minlen != sLen+1)
{
char* res = (char*)malloc(sizeof(char) * (minlen + 1));
*res = '\0';
strncat(res, s + start, minlen);
return res;
}
return "";
}
该代码也采用了用数组来维护窗口内的数据是否满足条件,但判断条件变为tLen==0,与数组中无元素的值大于0的效果是一样的
start用来返回满足条件的窗口的起始位置,最后用minlen来控制返回的字符串个数,因为要最后输出符合条件的字符串,所以需要开辟数组将目标字符串存储起来.
总结
- 示例应用场景: 最大子数组和问题: 寻找一个数组中的连续子数组,使得子数组的和最大。
- 最小覆盖子串问题: 找到包含指定字符的最短子串。
- 定长子数组的最大平均值问题: 寻找一个数组中长度为 k 的子数组,使得平均值最大。
- 连续K个元素的最大和问题: 寻找一个数组中连续 K 个元素的最大和。
- 总体而言,滑动窗口是一种灵活且强大的算法技巧,对于处理一些特定问题时能够提供高效的解决方案。