滑动窗口法从入门到精通
滑动窗口算法技巧主要用来解决子数组问题,比如让你寻找符合某个条件的最长/最短子数组,字符串寻找子串。
注意:如果数组元素全为正数可以用滑动窗口
,如果有负数则用前缀和+哈希表
,有序数组二分法
典型问题:
- 最大或最小子数组问题:比如最小子数组和最大子数组。
- 固定窗口的滑动:比如“滑动窗口内元素和大于某个数”。
- 动态窗口调整:如维护一个符合条件的动态窗口,如“滑动窗口中包含所有字母的最小窗口”。
示例:最大连续子数组和; 子数组乘积小于某个值; 最小窗口包含子串
补充:数组问题解决方法分类
二分法:用于查找满足某个条件的最小/最大值,通常在有序数据上使用。
前缀和:用于高效求解区间统计问题,特别是区间和/频率等问题。
滑动窗口:用于处理连续子数组/子序列的最优化问题或计数问题。
哈希:用于高效查找、去重、频率统计等问题。
滑动窗口算法的快慢指针特性:left
指针在后,right
指针在前,两个指针中间的部分就是「窗口」,算法通过扩大和缩小「窗口」来解决某些问题。
// 滑动窗口算法框架伪码
int left = 0, right = 0;
while (right < nums.size()) {
// 增大窗口
window.addLast(nums[right]);
right++;
while (window needs shrink) {
// 缩小窗口
window.removeFirst(nums[left]);
left++;
}
}
76. 最小覆盖子串
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
class Solution {
public:
string minWindow(string s, string t) {
//need作为t字符串需要统计的哈希对照,window是窗口里实时更新的哈希对照
unordered_map<char,int> need,window;
for(char c:t){
need[c]++;
}
int left=0,right=0;//[left,right]就是滑动窗口
int valid=0;//记录s和t里已完成一种字符的统计(a,b……)
int len=INT_MAX;//int类型最大数字
int re_start=0;//返回字符串的起点
while(right<s.size()){
char c = s[right];
right++;// 增大窗口
if(need.count(c)){//这个if作用是window里面只更新need里出现的字符
window[c]++;
if(need[c]==window[c]){
valid++;
}
}
while(valid==need.size()){
if(right-left<len){//更新后面要返回的re_start和len
re_start=left;
len=right-left;
}
char d = s[left];//窗口内要去除的那个字符
left++;
if(need.count(d)){
if(need[d]==window[d]){
valid--;
}
window[d]--;
}
}
}
return len==INT_MAX?"":s.substr(re_start,len);
//如果找到匹配的,len更小,返回对应字符串
}
};
567. 字符串的排列
给你两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的排列。如果是,返回true
;否则,返回false
。换句话说,s1
的排列之一是 s2
的 子串 。
示例 1:
输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").
示例 2:
输入:s1= "ab" s2 = "eidboaoo"
输出:false
class Solution {
public:
bool checkInclusion(string s1, string s2) {
//连续但不分正反-->哈希+滑动窗口
unordered_map<char,int> need,window;
for(char c:s1){
need[c]++;
}
int left=0,right=0;
int valid=0;
while(right<s2.size()){
char c=s2[right];
right++;
if(need.count(c)){
window[c]++;
if(need[c]==window[c]){
valid++;
}
}
//子串的出现要连续,所以窗口大小设定和子串长度一样进行滑动
while(right-left>=s1.size()){
if(valid==need.size()){//限定长度内相同字符出现相同次数为true
return true;
}
char d=s2[left];
left++;
if(need.count(d)){
if(need[d]==window[d]){
valid--;
}
window[d]--;
}
}
}
return false;
}
};
438. 找到字符串中所有字母异位词
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词的子串,返回这些子串的起始索引。不考虑答案输出顺序。
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
//哈希+滑动窗口
unordered_map<char,int> need,window;
for(char c :p ){
need[c]++;
}
int left=0,right=0;
int valid=0;
vector<int> result;
while(right<s.size()){
char c = s[right];
right++;
if(need.count(c)){
window[c]++;
if(need[c]==window[c]){
valid++;
}
}
while(right-left>=p.size()){
if(valid==need.size()){
result.push_back(left);
}
char d = s[left];
left++;
if(need.count(d)){
if(need[d]==window[d]){
valid--;
}window[d]--;
}
}
}return result;
}
};
3. 无重复字符的最长子串
给定一个字符串 s
,请你找出其中不含有重复字符的 最长 子串的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char,int> window;
int left=0,right=0;
int len=0;
while(right<s.size()){
char c = s[right];
right++;
window[c]++;
while(window[c]>1){
char d=s[left];
left++;
window[d]--;
}
if(right-left>len){
len=right-left;
}
}
return len;
}
};
209. 长度最小的子数组
给定一个含有 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
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left=0, right=0;
int result=INT_MAX;
int sumv=0;
while(right<nums.size()){
sumv +=nums[right];
right++;
while(sumv>=target){
sumv-=nums[left];
left++;
if (right - left + 1< result) {
result = right - left + 1;
}
}
}
return result==INT_MAX?0:result;
}
};
904. 水果成篮
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 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 = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。
示例 4:
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘 [1,2,1,1,2] 这五棵树。
翻译题意:寻找一个最长的连续子串,里面只有2种数字,返回其长度
class Solution {
public:
int totalFruit(vector<int>& fruits) {
unordered_map<int,int> window;
int left=0,right=0,len=0;
while(right<fruits.size()){
int a=fruits[right++];
window[a]++;
while(window.size()>2){
int d=fruits[left++];
window[d]--;
//这个是关键,调试的时候出bug地方
if (window[d] == 0) {
//只要为0一定抹掉
window.erase(d);
}
if(right-left>len){
len=right-left;
} //长度判断上下位置多次调试
}
if(right-left>len){
len=right-left;
}//这个没有,也出了一些bug
}
return len;
}
};
1658. 将 x 减到 0 的最小操作数
给你一个整数数组 nums
和一个整数 x
。每一次操作时,你应当移除数组 nums
最左边或最右边的元素,然后从 x
中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。
如果可以将 x
恰好 减到 0
,返回 最小操作数 ;否则,返回 -1
。
示例 1:
输入:nums = [1,1,4,2,3], x = 5
输出:2
解释:最佳解决方案是移除后两个元素,将 x 减到 0 。
示例 2:
输入:nums = [5,6,7,8,9], x = 4
输出:-1
示例 3:
输入:nums = [3,2,20,1,1,3], x = 10
输出:5
解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。
**思路:**这道题等价于让你寻找 nums 中元素和为 sum(nums) - x 的最长子数组。
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int allsum = 0;
for(int i=0; i<nums.size(); i++){
allsum+=nums[i];
}
int target = allsum-x;//题意转化为求这个变量的最长字符长度
int length=INT_MIN;
int slow=0, fast=0;
int sum=0;
while(fast<nums.size()){
sum+=nums[fast];
fast++;
while(sum>target&&slow<fast){
sum-=nums[slow];
slow++;
}
if(sum==target){
//fast加入sum后指向下一位,所以fast不是指向sum里元素
length = max(length,(fast-slow));
}
}
return length==INT_MIN?-1:(nums.size()-length);
}
};
713. 乘积小于 K 的子数组
给你一个整数数组 nums
和一个整数 k
,请你返回子数组内所有元素的乘积严格小于k
的连续子数组的数目。
示例 1:
输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2]、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。
示例 2:
输入:nums = [1,2,3], k = 0
输出:0
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
int n=nums.size();
int left=0, right=0;
int ji=1;
int res=0;
while(right<n){
ji*=nums[right];
right++;
if(ji<k&&left<=right){
res+=right-left;//最关键的就是这一步
}
while(ji>=k&&left<right){
ji/=nums[left];
left++;
if(ji<k&&left<=right){
res+=right-left;//最关键的就是这一步
// 现在必然是一个合法的窗口,但注意思考这个窗口中的子数组个数怎么计算:
// 比方说 left = 1, right = 4 划定了 [1, 2, 3] 这个窗口(right 是开区间)
// 但不止 [left..right] 是合法的子数组,[left+1..right], [left+2..right] 等都是合法子数组
// 所以我们需要把 [3], [2,3], [1,2,3] 这 right - left 个子数组都加上
}
}
}return res;
}
};
1004. 最大连续1的个数 III
给定一个二进制数组 nums
和一个整数 k
,如果可以翻转最多 k
个 0
,则返回 数组中连续 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。
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int left = 0, right = 0;
int n=nums.size();
int count = 0;
int length=0;
while(right<n){
int a = nums[right];
right++;
if(a==0){
count++;
}
if(count<=k){
length = max(length, right-left);
}
while(count>k){
int b = nums[left];
if(b == 0){
count--;
}
left++;
if(count<=k){
length = max(length, right-left);
}
}
}return length;
}
};
424. 替换后的最长重复字符
给你一个字符串 s
和一个整数 k
。你可以选择字符串中的任一字符,并将其更改为任何其他大写英文字符。该操作最多可执行 k
次。
在执行上述操作后,返回 包含相同字母的最长子字符串的长度。
示例 1:
输入:s = "ABAB", k = 2
输出:4
解释:用两个'A'替换为两个'B',反之亦然。
示例 2:
输入:s = "AABABBA", k = 1
输出:4
解释:
将中间的一个'A'替换为'B',字符串变为 "AABBBBA"。
子串 "BBBB" 有最长重复字母, 答案为 4。
可能存在其他的方法来得到同样的结果。
class Solution {
public:
int characterReplacement(string s, int k) {
int left = 0, right = 0;
int n=s.size();
vector<int> window(26,0);
int winMaxcount=0;
int res=0;
while(right<n){
char a = s[right];
window[a-'A']++;
winMaxcount = max(winMaxcount,window[a-'A']);
right++;
if(right-left-winMaxcount<=k){
res=max(res,right-left);
}
while(right-left-winMaxcount>k){
char b = s[left];
window[b-'A']--;
left++;
res=max(res,right-left);
}
}return res;
}
};
219. 存在重复元素 II
给你一个整数数组 nums
和一个整数 k
,判断数组中是否存在两个 不同的索引 i
和 j
,满足 nums[i] == nums[j]
且 abs(i - j) <= k
。如果存在,返回 true
;否则,返回 false
。
示例 1:
输入:nums = [1,2,3,1], k = 3
输出:true
示例 2:
输入:nums = [1,0,1,1], k = 1
输出:true
示例 3:
输入:nums = [1,2,3,1,2,3], k = 2
输出:false
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
//维护一个right-left=k的窗口,如果里面出现了则true,没有则false
int left=0, right=0;
unordered_set<int> map;
while(right<nums.size()){
int a = nums[right];
right++;
if(map.find(a)!=map.end()){
return true;
}
map.insert(a);
while(right-left>k){
int b = nums[left];
map.erase(b);
left++;
}
}return false;
}
};