滑动窗口
文章前面总结性概括了一些模版,后面有案例去套用模版。
食用方法推荐:先浏览一遍总结,心里有个底,然后去看案例如何应用模版解题,最后再看遍总结深刻体会一下。
什么时候使用滑动窗口?
适合用滑动窗口解决的问题的前提:连续的子数组,字串,子序列。要连续。
四步走:
-
Step 1: 定义需要维护的变量们 (对于滑动窗口类题目,这些变量通常是最小长度,最大长度,或者哈希表)
-
Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
- start = 0;
- end 是for i
里的i
-
Step 3: 更新需要维护的变量, 有的变量需要一个
if
语句来维护 (比如最大最小长度) -
Step 4 - 情况1 长度固定。if
-
Step 4 - 情况1 长度不固定。while 不合法
-
step 5 返回
基本模版
class Solution:
def problemName(self, s: str) -> int:
# Step 1: 定义需要维护的变量们 (对于滑动窗口类题目,这些变量通常是最小长度,最大长度,或者哈希表)
x, y = ..., ...
# Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
start = 0
for end in range(len(s)):
# Step 3: 更新需要维护的变量, 有的变量需要一个if语句来维护 (比如最大最小长度)
x = new_x
if condition:
y = new_y
'''
------------- 下面是两种情况,读者请根据题意二选1 -------------
'''
# Step 4 - 情况1
# 如果题目的窗口长度固定:用一个if语句判断一下当前窗口长度是否达到了限定长度
# 如果达到了,窗口左指针前移一个单位,从而保证下一次右指针右移时,窗口长度保持不变,
# 左指针移动之前, 先更新Step 1定义的(部分或所有)维护变量
if 窗口长度达到了限定长度:
# 更新 (部分或所有) 维护变量
# 窗口左指针前移一个单位保证下一次右指针右移时窗口长度保持不变
# Step 4 - 情况2
# 如果题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
# 如果当前窗口不合法时, 用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
# 在左指针移动之前更新Step 1定义的(部分或所有)维护变量
while 不合法:
# 更新 (部分或所有) 维护变量
# 不断移动窗口左指针直到窗口再次合法
# Step 5: 返回答案
return ...
本文介绍一下滑动窗口长度变化的几种情况并且给出例子:
- 最小
-
- 长度最小的子数组
-
- 最小覆盖子串
-
- 最大
- 3.请你找出其中不含有重复字符的 最长子串 的长度
- 485 最大连续1的个数
- ≤k
-
- 存在重复元素 II
-
- 最大连续1的个数 III
-
209. 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
本质上就是维护滑动窗口里的子数组。右指针向前为了寻找和最大(>= target),左指针向前为了符合要求:长度最小。
- 用 sum 记录窗口里的和
- 右指针往前移时就 维护sum,
- while 在保证窗口满足条件的情况下使得窗口最小化
var minSubArrayLen = function(target, nums) {
let res = +Infinity;
let left = 0;
let sum = 0;
for(let i = 0;i<nums.length;i++){
// 右指针扩大窗口,维护窗口的值
sum += nums[i];
while(sum >= target){
// 更新结果
res = Math.min(res, i-left+1)
// 左指针缩小窗口,维护窗口的值
sum -= nums[left];
left++;
}
}
return res===+Infinity ? 0 : res;
};
76. 最小覆盖子串
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
思路:
窗口要满足的条件:含有 t
所有字母,并且字母的数量是要大于等于 t
中该字母出现的次数。
需要用一个map记录 t
中出现的字母和对应的个数。
用一个winMap维护窗口里的和 t
中出现的字母一致的字母和个数。
如何判断窗口中的字符串是否包含t中的所有字母,并且字母的数量是大于等于t中出现的次数的?
⇒ 用一个count,当一个字母满足条件,则count ++ ,当count 的数量=== t中的字母的数量时即为满足条件。
var minWindow = function (s, t) {
let tMap = new Map();
for (let c of t) {
tMap.set(c, (tMap.get(c) || 0) + 1);
}
let count = 0;
let winMap = new Map();
let len = +Infinity;
let res = "";
let left = 0;
for(let i = 0; i < s.length; i++){
// 右指针扩大窗口,维护窗口的值
if(tMap.has(s[i])){
winMap.set(s[i], (winMap.get(s[i]) || 0) + 1);
if(winMap.get(s[i]) === tMap.get(s[i])){
count ++;
}
}
while(count === tMap.size){
// 更新结果
if (i - left + 1 < len) {
res = s.substring(left,i+1);
len = i - left + 1;
}
// 左指针缩小窗口,维护窗口的值
if(winMap.has(s[left])){
winMap.set(s[left], winMap.get(s[left]) - 1);
if(winMap.get(s[left]) < tMap.get(s[left])){
count --;
}
}
left++;
}
}
return res;
}
console.log(minWindow("ADOBECODEBANC", "ABC"));
3.请你找出其中不含有重复字符的 最长子串 的长度。
本质上就是滑动窗口里的字符串。右指针向前为了寻找最长,左指针向前为了符合要求:不含有重复的。可以用一个set
维护滑动窗口里的字母,如果窗口里没有右指针里的字母,则加入到窗口里,但是如果窗口里有右指针指向的字母,则必须移动左指针使得窗口满足不含有重复字母的条件。
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
let max = 0;
let set = new Set();
let left = 0;
for(let i = 0; i < s.length; i++){
// 左指针缩小窗口,维护窗口的值
while(set.has(s[i])){
set.delete(s[left])
left++
}
// 右指针扩大窗口,维护窗口的值
set.add(s[i])
// 更新结果
max = Math.max(max, set.size);
}
return max;
};
485 最大连续1的个数
给定一个二进制数组 nums
, 计算其中最大连续 1
的个数
- 右指针向右移动扩大1的个数。
- 左指针向右移动维持1的个数是连续的。
思路:
窗口维护1的个数,如果遇到不是1的则缩小窗口。
var findMaxConsecutiveOnes = function(nums){
let count = 0;
let max = 0;
for(let i = 0; i < nums.length; i++){
// 扩大窗口,增加1的个数
if(nums[i] === 1){
count++;
// 更新结果
max = Math.max(max, count);
}
// 缩小窗口,不必通过while逐个单位减少。
if(nums[i] === 0){
count = 0;
}
}
return max;
}
219. 存在重复元素 II](长度不固定但是要小于某个数)
给你一个整数数组 nums
和一个整数 k
,判断数组中是否存在两个 不同的索引 i
和 j
,满足 nums[i] == nums[j]
且 abs(i - j) <= k
。如果存在,返回 true
;否则,返回 false
。
var containsNearbyDuplicate = function(nums, k) {
let set = new Set();
let left = 0;
for(let right = 0;right<nums.length;right++){
// 扩大右边窗口
if(set.has(nums[right])){
// 更新结果
return true;
}
set.add(nums[right]);
// 缩小左边窗口
if(right - left +1 > k){
set.delete(nums[left]);
left++;
}
}
return false;
};
1004. 最大连续1的个数 III (长度不固定但是要小于某个数)
给定一个二进制数组 nums
和一个整数 k
,如果可以翻转最多 k
个 0
,则返回 数组中连续 1
的最大个数 。
窗口维护1的个数,窗口的长度要小于 count + k、
var longestOnes = function(nums, k) {
let left = 0;
let count = 0;
let max = 0;
for(let i = 0; i < nums.length; i++){
// 扩大窗口
if(nums[i] == 1){
count++;
}
// 长度要小于某个值,缩小窗口
if(i - left + 1 - count > k){
if(nums[left] == 1){
count--;
}
left++;
}
// 更新结果
max = Math.max(max, i - left + 1);
}
return max;
};