滑动窗口法从入门到精通

滑动窗口法从入门到精通

滑动窗口算法技巧主要用来解决子数组问题,比如让你寻找符合某个条件的最长/最短子数组,字符串寻找子串。

注意:如果数组元素全为正数可以用滑动窗口,如果有负数则用前缀和+哈希表,有序数组二分法

典型问题:

  • 最大或最小子数组问题:比如最小子数组和最大子数组。
  • 固定窗口的滑动:比如“滑动窗口内元素和大于某个数”。
  • 动态窗口调整:如维护一个符合条件的动态窗口,如“滑动窗口中包含所有字母的最小窗口”。

示例:最大连续子数组和; 子数组乘积小于某个值; 最小窗口包含子串

补充:数组问题解决方法分类

二分法:用于查找满足某个条件的最小/最大值,通常在有序数据上使用。

前缀和:用于高效求解区间统计问题,特别是区间和/频率等问题。

滑动窗口:用于处理连续子数组/子序列的最优化问题或计数问题。

哈希:用于高效查找、去重、频率统计等问题。


滑动窗口算法的快慢指针特性: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. 字符串的排列

给你两个字符串 s1s2 ,写一个函数来判断 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. 找到字符串中所有字母异位词

给定两个字符串 sp,找到 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,如果可以翻转最多 k0 ,则返回 数组中连续 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 ,判断数组中是否存在两个 不同的索引 ij ,满足 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;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值