这个专题我们讲滑动窗⼝
滑动窗口: 就是用一个可变长度的框,在数组上从左往右滑动,框住一块连续的区域,根据情况随时调整框的左右边界,从而找到符合条件的子数组或子串。
简单理解就是:
想象你在一条长绳子上找一段最短的、长度超过1米的绳子。你用左手和右手在绳子上比划,左手不动,右手往右伸,直到两手之间的绳子够长了,就记录一下长度;然后左手往右缩一点,看看还够不够长,不够了右手再往右伸......如此反复,两只手交替往右移动,这就是滑动窗口。
1.⻓度最⼩的⼦数组
题⽬链接: 209. 长度最小的子数组 - 力扣(LeetCode)
题目描述:

算法原理:
解法⼀暴⼒求解(会超时):
算法思路:
从前往后枚举数组中的任意⼀个元素,把它当成起始位置。然后从这个起始位置开始,然后寻找⼀段最短的区间,使得这段区间的和**⼤于等于⽬标值**。
将所有元素作为起始位置所得的结果中,找到最⼩值即可。
cpp
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;
}
};
解法⼆(滑动窗⼝):
核心思想: 利用单调性(数组元素为正数),两个指针同向移动不回退,通过不断调整窗口的左右边界,在 O(n) 时间内找到满足和 ≥ target 的最短连续子数组。
算法思路:
由于此问题分析的对象是**⼀段连续的区间**,因此可以考虑**滑动窗⼝**的思想来解决这道题。
让滑动窗⼝满⾜:从 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) 。
cpp
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;
}
};
过程展现:

问题补充:
问1:为什么 len 一开始要赋值为 INT_MAX?
答:因为要找最小值,初始必须给一个最大的数 作为参照,这样第一次遇到有效答案时,
min(INT_MAX, 有效长度)才能正确更新为有效长度。如果初始给 0,那min(0, 任何正数)永远都是 0,结果就错了。
问2:最后为什么要用三元运算符判断 len == INT_MAX ? 0 : len?
答:因为有可能整个数组加起来都达不到
target,此时while循环一次都没进去过,len始终没被更新,还是原来的INT_MAX。如果不判断直接返回,就会返回一个巨大的错误值,所以要先检查:如果还是INT_MAX,说明没找到,按题目要求返回 0。
问3:这两个操作有关系吗?
答:有。初始给
INT_MAX是为了让第一次更新能成功,最后判断是不是INT_MAX是为了区分找到了但长度恰好是 INT_MAX(但不可能是这个最大值,这里只是举例子)和根本没找到这两种情况。两者配合使用,是这类题的标准写法。
2. ⽆重复字符的最⻓⼦串
题⽬链接: 3. 无重复字符的最长子串 - 力扣(LeetCode)
这里给大家补充一个概念,子串和子数组二者的区别,都是一段连续的区间,只不过子串里面是字符,子数组里面是整数
题目描述:

算法原理:
解法⼀暴⼒求解(不会超时,可以通过):
算法思路:
枚举从每⼀个位置开始往后,⽆重复字符的⼦串可以到达什么位置。找出其中⻓度最⼤的即
可。在往后寻找⽆重复⼦串能到达的位置时,可以利⽤哈希表统计出字符出现的频次,来判断什么时候⼦串出现了重复元素。
cpp
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;
}
};
解法⼆(滑动窗⼝)
算法思路:
研究的对象依旧是⼀段连续的区间,因此继续使**⽤滑动窗⼝**思想来优化。
让滑动窗⼝满⾜:窗⼝内所有元素都是不重复的。
做法:
先定义两个指针刚开始指向同一个位置,左指针不动,右指针往前走,每走一步就把新字符记到哈希表里,如果发现这个字符出现次数超过1了,说明窗口里有重复,那左指针就往右挪,一边挪一边把丢掉的字符次数减1,直到这个新字符的次数变回1,窗口重新变合法;如果本来就没超过1,那窗口本来就是合法的。不管哪种情况,只要窗口合法了,就用当前窗口的长度去更新最大长度,右指针继续往前走,直到遍历完整个字符串。最后返回记录的最大长度就行。
举个例子说明一下:
s="abcabcbb"
right走到a,窗口"a",len=1
right走到b,窗口"ab",len=2
right走到c,窗口"abc",len=3
right走到a,发现a上次在位置0,还在窗口里,left跳到1,窗口变成"bca",len还是3
right走到b,发现b上次在位置1,还在窗口里,left跳到2,窗口变成"cab",len还是3
后面怎么走长度都不会超过3,最后返回3
cpp
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;
}
};
过程展示:

问题补充:
问1:hash 在模拟什么数组?
答:在模拟哈希表,用数组的索引当键(字符的 ASCII 码),数组的值当数据(字符出现的次数)。
问2:那它和普通数组有啥区别?
答:普通数组比如
int a[10],下标是 0~9,存的是任意数据。hash数组的下标是 ASCII 码(0~127),存的是出现次数,用法上把它当成字符到次数的映射表,所以又叫模拟哈希表。
问3:hash[s[right]]++在干啥和hash[s[left++]]--是干啥?
答:
hash[s[right]]++举个例子,s[right]是'a',ASCII码是97,这一句就是hash[97] = hash[97] + 1,把第97个格子的数字加1;
hash[s[left++]]--先取s[left],比如是'a',还是第97个格子,然后hash[97] = hash[97] - 1,把数字减1,再把left加1。这就是出窗口,把左边字符踢出去,计数减1。
问:那进窗口和出窗口都在操作同一个格子?
答:是的,同一个字符进进出出,操作的都是同一个格子。比如窗口里有一个
'a',hash['a']就是1;再进来一个'a',变成2;出去一个'a',又变回1。
注意:
窗口 :由
left和right指针框出的那段连续区间,是实实在在的子串。hash:用来记录窗口里每个字符出现了几次,是窗口的计数器。
3.最⼤连续 1 的个数 III
题⽬链接 :1004. 最大连续1的个数 III - 力扣(LeetCode)
题目描述:

算法原理:
解法:滑动窗⼝
算法思路:
看到这道题首先,不要去想怎么翻转,不要把问题想的很复杂,这道题的结果⽆⾮就是⼀段连续的 1 中间塞了 k个 0 嘛。
因此,我们可以把问题转化成:求数组中⼀段最⻓的连续区间,要求这段区间内 0 的个数不超过 k 个。
那好,既然是连续区间,可以考虑使⽤滑动窗⼝来解决问题。
算法流程:
a. 初始化⼀个⼤⼩为 2 的数组就可以当做哈希表 hash 了;初始化⼀些变量 left = 0 ,
right = 0 , ret = 0 ;
b. 当 right ⼩于数组⼤⼩的时候,⼀直下列循环:
1.让当前元素进⼊窗⼝,顺便统计到哈希表中;
- 检查 0 的个数是否超标:
如果超标,依次让左侧元素滑出窗⼝,顺便更新哈希表的值,直到 0 的个数恢复正常;
3.程序到这⾥,说明窗⼝内元素是符合要求的,更新结果;
- right++ ,处理下⼀个元素;
c. 循环结束后, ret 存的就是最终结果。
简单理解就是:别管翻转这个动作,本质就是找一段最长的连续区间,里面 0 的个数不超过 k 个。右指针一直往右走加元素,用 zero 记录窗口里 0 的个数,如果 zero 超过 k 了,左指针就往右挪,把丢掉的 0 减掉,直到 zero 回到 ≤ k,此时窗口合法,用当前长度更新最大值。右指针继续走,直到遍历完整个数组。
过程展示:

时间 复杂度O(n),空间复杂度 O(1)
4.将 x 减到 0 的最⼩操作数
题⽬链接: 1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)
题目描述:

算法原理:
解法(滑动窗⼝)
算法思路:
题⽬要求的是数组左端+右端两段连续的、和为 x 的最短数组,信息量稍微多⼀些,不易理清思路;我们可以转化成求数组内⼀段连续的、和为 sum(nums) - x 的最⻓数组。此时,就是熟悉的滑动窗⼝问题了。
算法流程:
a. 转化问题:求 target = sum(nums) - x 。如果 target < 0 ,问题⽆解;
b. 初始化左右指针 left = 0 , right = 0 (滑动窗⼝区间表⽰为 [left, right) ,左右区间是否开闭很重
要,必须设定与代码⼀致),记录当前滑动窗⼝内数组和的变量 sum = 0 ,记录当前满⾜条
件数组的最⼤区间⻓度 ret = -1 ;
c. 当 right ⼩于等于数组⻓度时,⼀直循环:
- 如果 sum < target ,右移右指针,直⾄变量和⼤于等于 target ,或右指针已经移到
头;
- 如果 sum > target ,右移左指针,直⾄变量和⼩于等于 target ,或左指针已经移到
头;
3.. 如果经过前两步的左右移动使得 sum == target ,维护满⾜条件数组的最⼤⻓度,并
让下个元素进⼊窗⼝;
d. 循环结束后,如果 ret 的值有意义,则计算结果返回;否则,返回 -1 。
举例子
nums = [1,1,4,2,3],x = 5总和
sum = 11
target = 11 - 5 = 6,就是要找中间一段连续子数组,和等于 6找最长的:
[1,1,4]和是 6,长度 3,或者[4,2]和是 6,长度 2,最长的长度是 3最少拿的次数 = 总长度 5 - 最长剩下长度 3 = 2
验证一下:从左边拿
1和1,从右边拿3,拿走的和是1+1+3=5,拿了 2 次,过程没错
过程展现:

问题补充:
问:滑动窗口在找啥?
答:找窗口内元素和
tmp等于target的最长窗口。用滑动窗口把所有和等于target的子数组都找一遍,记录最大长度。
问:while(tmp > target) 是干啥?
答:如果窗口和
tmp已经超过target了,因为数组元素都是正数,继续往右扩只会更大,所以必须从左边缩小窗口,直到tmp <= target。
问:if(tmp == target) 更新结果是啥?
答:找到一段和正好等于
target的子数组,用ret记录它的最大长度。
到这里我们会发现,滑动窗口就可以总结一个模板,但是模板还是来自对于问题的思考,这个题能不能用模板,一般是根据单调性加指针我们推导出可以用模板,我总结的模板如下:
核心思想总结:
用两个指针维护一个会动的区间,右指针负责扩张试探,左指针负责收缩纠偏,区间合法时就记录答案,整个过程像一扇从左滑到右的窗户。
三步走:
右指针扩张:不断把新元素纳入窗口,直到窗口过大或不满足条件;
左指针收缩:当窗口不满足条件时,移动左指针缩小窗口,直到窗口重新满足条件;
更新答案:在窗口满足条件的时刻,用当前窗口的信息(如长度、和等)更新最终答案。