滑动窗口算法解释与例题

        理解:滑动窗口是双指针的一种应用,本质上是维护了一段数据区间,区间的左右端点移动是单调的。具体含义是:一个指针移动,另一个指针只能往一个方向移动,不能两个方向来回移动。 一般是去解决一些子数组,子串的问题的。

        实质:通过发现题目的一些单调性质,对暴力循环的一种简化,时间复杂度从平方降到一次方。

        实现:可以基于双端队列,也可以基于左右指针,优先考虑左右指针更节省空间,但是如果需要对窗口中的元素进行一定的处理操作,那么选择双端队列实现。左右指针是通过左指针代表左边界,右指针代表右边界,二者同时向右移动的基础原理。双端队列是左端点出数,右端点进数,不断的将元素入队和出队来实现的。

题目:

       力扣209 :长度最小的子数组

        209. 长度最小的子数组

        如果我们用暴力做法去做的话,我们需要去分别枚举子数组的左右端点。但我们可以发现很多子数组是没有必要去枚举的。例如:如果一个子数组的和都已经超过了tar了,我们还往后枚举,其实已经没有必要了。

        我们可以观察到一个二段性质:子数组的和 ① >=tar ② <tar 依据此性质可以建立滑动窗口。

code: 

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int ans = INT_MAX;
        int sum = 0;
        for(int i = 0,j = 0;j<nums.size();j++)
        {
            sum += nums[j];
            while(sum>=target)
            {
                ans = min(j-i+1,ans);
                sum -= nums[i];
                i++;
            }
        }
        return ans == INT_MAX? 0 :ans;
    }
};

细节:  为什么用while不用if? while也可以起到if的作用,而且是连续性的if。如果只有if,可能会导致只删除了一个数以后,子数组的和还是满足条件。

力扣 713: 乘积小于k的子数组

        713. 乘积小于 K 的子数组

        跟上一题一样的思路,只是特判一下边界即可。

code:

class Solution {
public:
    int numSubarrayProductLessThanK(vector<int>& nums, int k) {
        if(k<=1) return 0;
        int sum = 1 ;
        int ans = 0;
        for(int i = 0,j = 0;j<nums.size();j++)
        {
            sum*=nums[j];
            while(sum>=k)
            {
                sum/=nums[i];
                i++;
            }
            ans += j-i+1;
        }
        return ans;
    }
};

细节: 固定了右端点以后,[l,r]有效的集合怎么算?[l-1,r]...[r,r]都是有效的集合,所以就是r-l+1

力扣3 无重复字符的最长子串:

3. 无重复字符的最长子串

怎么记录每个字母出现的次数?用哈希表

code: 

class Solution {
public:
    int map[1000];
    int lengthOfLongestSubstring(string s) {
        int n = s.size();
        int ans = 0;
        for(int i = 0,j = 0;j<n;j++)
        {    
            map[s[j] ]++;
            while(map[s[j]] > 1)
            {
                map[s[i]]--;
                i++;
            }
           ans = max(ans,j-i+1);
        }
        return ans;
    }
};

细节:这里用数组模拟哈希表了,原理是记录字符的ASCII值。

力扣904: 水果成篮

904. 水果成篮

也是用滑动窗口就可以解决了的问题。但这里用数组模拟就不太可行了,我们用unordered_map去记录每种水果出现的次数就行了。最后判断map.size()是否大于2即可。

code:

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        unordered_map<int,int> mp;
        int n = fruits.size();
        int ans = 0;
        for(int i = 0,j = 0;j<n;j++)
        {
            mp[fruits[j]]++;
            while(mp.size()>2)
            {
                mp[fruits[i]]--;
                if(mp[fruits[i]] == 0) mp.erase(fruits[i]);
                i++;
            }
            ans = max(ans,j-i+1);
        }
        return ans;
    }
};

细节: unordered_map加入元素可以用mp[i]++,如果原来没有i这个key,会自动加上。删除元素如果只是mp[i]--,那么只会删除key对应的value的值,删除key要用到erase函数。

力扣76: 最小覆盖子串

76. 最小覆盖子串

        这个题目需要记录的窗口中的子串里面需要有t串中所有的元素,其实种类数量都需要对应上。这不是正好可以用哈希表去解决吗

        我们可以开两个哈希表,一个是s中出现的元素种类与个数,另一个记录t中出现的种类与个数。

        另外还得开一个cnt变量,去记录当前窗口中满足t串元素的个数。因为如果当前s中只有t串元素的子集,那是不能更新答案的。

        右端点前进,元素放入s哈希表中,如果遇到了t中的元素,这个时候我们需要增加cnt。因为这是第一次遇见,所以这个元素一定是需要的。

        同时我们需要考虑左端点的值,如果左端点的对应的值已经超过t中某元素的数量上限,我们需要剔除该元素并前移一位。

        当cnt == t.size()的时候,说明我们可以收集答案了。答案就是ans与当前窗口长度的最小值。

        code:

class Solution {
public:
    int cnt;
    string minWindow(string s, string t) {
        unordered_map<char,int> mps,mpt;
        string ans;
        for(auto i:t) mpt[i]++;
        
        for(int i = 0,j = 0;j<s.size();j++)
        {
            mps[s[j]]++;
            if(mps[s[j]]<=mpt[s[j]]) cnt ++;

            while(mps[s[i]]>mpt[s[i]])
                mps[s[i++]]--;
            if(cnt == t.size())
            {
                if(!ans.size()||j-i+1<ans.size())
                    ans = s.substr(i,j-i+1);
            }
        }
        return ans;
    }
};

acwing 3624 三值字符串

3624. 三值字符串 - AcWing题库

        也是一道模板题。关键是记录元素的种类已经个数。当元素个数到达3个的时候开始考虑移动左端点。

        code:

#include<iostream>
#include<algorithm>
#include<unordered_map>
using namespace std;

unordered_map<char,int> mp;
int main()
{
    int m;
    cin>>m;
    while(m--)
    {
        mp.clear();
        string s;
        cin>>s;
        int ans = 2*1e6;
        for(int i = 0,j = 0;j<s.size();j++)
        {
            mp[s[j]]++;
            while(mp[s[i]]>1 &&mp.size()==3) mp[s[i++]]--;
            if(mp.size()==3)ans = min(ans,j-i+1);
        }
        if(ans == 2*1e6) ans =0;
        cout<<ans<<endl;
    }
    return 0;
}

力扣187:重复的DNA序列

187. 重复的DNA序列

按题意直接模拟一个窗口。

code:

class Solution {
public:
    vector<string> ans;
    unordered_map<string,int> map;
    vector<string> findRepeatedDnaSequences(string s) {
        if(s.size()<=10) return ans;   
        for(int l = 0,r = 9;r<s.size();r++)
        {
            while(r-l+1>10) l++;
            string str = s.substr(l,r-l+1);
            map[str]++;
            if(map[str]==2) ans.push_back(str);
        }
        return ans;
    }
};

力扣219:存在重复元素Ⅱ

219. 存在重复元素 II

        感觉跟滑动窗口很像,但本质就是用哈希记录每个端点出现的下标。

        code: insert是为了方便处理,下标从1开始。

class Solution {
public:
    unordered_map<int,int> mp;
    bool containsNearbyDuplicate(vector<int>& nums, int k) {
        nums.insert(nums.begin(),0);
        int n = nums.size();
        for(int i = 1;i < n;i++)
        {
            if(mp[nums[i]]) 
            {
                if(i - mp[nums[i]]<=k)
                    return 1;
            }
            mp[nums[i]] = i;
        }
         return 0;
    }
   
};

力扣395:至少有k个重复字符的最长子串

        395. 至少有 K 个重复字符的最长子串

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

        这题很特殊,由于我们不知道区间可能会出现几种字母,所以我们很难用双指针。但是考虑到一共就26个字母,所以我们直接26次暴力枚举一下,然后每种情况滑动窗口一下就行了   

  1. 右端点往右移动必然会导致字符类型数量增加(或不变)
  2. 左端点往右移动必然会导致字符类型数量减少(或不变)

当遇到新元素,diff_cnt++,否则就不是新元素,那该元素的cnt++;当我们遇到的元素种类大于i的时候,开始缩小窗口。

        code:

class Solution {
public:
    int map[26];
    int longestSubstring(string s, int k) {
        int ans = 0;
        for(int i = 1;i<=26;i++)
        {
            memset(map,0,sizeof (map));
            int diff_cnt = 0,cnt = 0;
            for(int l = 0, r = 0;r<s.size();r++)
            {
                int add_idx = s[r] - 'a';
                map[add_idx]++;
                if(map[add_idx] == 1) diff_cnt++;
                if(map[add_idx] == k) cnt++;

                while(diff_cnt > i)
                {
                    int del_idx = s[l] - 'a';
                    if(map[del_idx]==k) cnt--;
                    if(map[del_idx]==1) diff_cnt--;
                    map[del_idx]--;
                    l++;
                }
                if(diff_cnt == i && diff_cnt == cnt)
                    ans = max(ans,r-l+1);
            }
        }
        return ans;
    }
};

力扣438,LCR015: 找到字符串中的所有字母异位词

        438. 找到字符串中所有字母异位词

        这里要维护的集合需要额外的两个变量,一个是需要的字符数量cnt,另一个是目前多余的字符数量diff_cnt。用两个哈希表去记录,一个记录字符串p中出现的元素,另一个记录当前窗口中出现的元素次数。

        当窗口右移的时候,哈希表加入当前的字符,先判断是否这个字符是否是需要的字符,如果满足maps[s[j]] <= mapp[s[j]],说明是需要这个字符的,如果不满足这个条件,有两个可能:第一:需要字符的种类已经超出需要的数目了,第二:出现了不需要字符的种类。这个时候diff_cnt就要加了。

        左指针移动的条件是:当左指针指向的字符的数目已经大于需要的字符数目的时候,就要移动了。移动的时候要记得删除cnt与diff_cnt的数目。

        收集结果:必须满足cnt == p.size()且diff_cnt == 0,说明当前维护的区间里面的值没有多余的字符。

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> ans;
        int n = s.size(),m = p.size();
        if(n<m) return ans;

        unordered_map<char,int> maps,mapp;
        for(auto i:p) mapp[i]++;

        int cnt = 0,diff_cnt = 0; //cnt当前已经有的目标字符个数,diff_cnt表示窗口内的
        for(int i = 0,j = 0;j<n;j++)
        {
            maps[s[j]]++;
            if(maps[s[j]] <= mapp[s[j]]) cnt ++;
            else diff_cnt++;

            while(maps[s[i]]>mapp[s[i]])
            {
                if(maps[s[i]] > mapp[s[i]]) diff_cnt--;
                maps[s[i++]]--;
            }
            if(cnt == p.size()&&diff_cnt == 0)
                ans.push_back(i);
        }
        return ans ;
    }
};

力扣930,2799:和相同的二元子数组 统计完全子数组的数目

        2799. 统计完全子数组的数目     

          关键点就是当我们找到满足的数组以后,包括该数组的左边数组其实都是满足要求的!

        所以我们移动左端点的原则就是让当前区间是最小的合法区间。那必须是要满足两个条件:第一:区间里面数的种类达到k个,第二就是每个数的数量必须为1。所以是mp.size() == k && mp[nums[i]]>1。

        统计结果的时候我们也需要满足mp.size() == k。

class Solution {
public:
    // 左边的数都是满足要求的!!!
    int countCompleteSubarrays(vector<int>& nums) {
        unordered_set<int> st(nums.begin(),nums.end());
        int k = st.size();
        unordered_map<int,int> mp;
        int ans = 0;
        for( int i = 0,j=0;j<nums.size();j++)
        {
            mp[nums[j]]++;
            while(mp.size() == k && mp[nums[i]]>1)
            {
                mp[nums[i++]]--;
            }
            if(mp.size() == k)ans += i+1;
        }
        return ans;
    }
};

力扣2260: 必须拿起的最小连续卡牌数

        2260. 必须拿起的最小连续卡牌数

        这个比较模板,只需要记录一下最小的答案就行了。判断条件就是右指针指向的数出现了两次,那就可以缩小区间了,最后那个区间长度就是答案。

class Solution {
public:
    int map[1000005];
    int minimumCardPickup(vector<int>& cards) {
        int ans = INT_MAX;
        int n = cards.size();
        for(int i = 0,j = 0;j<n;j++)
        {
            map[cards[j]]++;
            while(map[cards[j]]>1) 
            {
                ans = min(ans,j-i+1);
                map[cards[i++]]--;
            }
        }
        return ans == INT_MAX?-1:ans;
    }
};

力扣1234: 替换子串得到平衡字符串

        1234. 替换子串得到平衡字符串

        反向思维:先记录所有字符出现的次数。

        题目都很难读懂。 意思就是把分成两个区间,一个是可替换区间,这个以外的是不可替换区间。只有当不可替换区间的每种字母数目不超过m/4才可以用可替换区间里面的字符去变化。所以我们左端点移动的条件就是每种字母数目不超过m/4。

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

        

class Solution {
public:
    int map[1000];
    int balancedString(string s) {
        int n = s.size(), m = n/4;
        for(auto i: s) map[i]++;
        int ans = n;
        if(map['Q'] == m && map['W'] == m && map['E'] == m && map['R'] == m) return 0;
        for(int i = 0,j = 0;j<n;j++)
        {
            map[s[j]] -- ;
            while(map['Q'] <= m && map['W'] <= m && map['E'] <= m && map['R'] <= m)
            {
                ans = min(ans,j-i+1);
                map[s[i++]]++;
            }
        }
        return ans;
    }
};

力扣1456:定长子串中的元音的最大数目

        1456. 定长子串中元音的最大数目

        模拟一个滑动窗口就可以了,哈希表记录窗口里面的元音的次数。

class Solution {
public:
    int map[1000];
    int maxVowels(string s, int k) {
        int n = s.size();
        if(n<k) return 0;
        int ans = 0;
        for(int i = 0;i<k-1;i++) map[s[i]]++;
        for(int i = 0,j = k-1;j<s.size();j++)
        {
            map[s[j]]++;
            while(j - i > k -1)
               map[s[i++]]--; 
            int sum = map['a'] + map['e']+map['i']+map['o']+map['u'];
            ans = max(sum,ans);
        }
        return ans;
    }
};

力扣1658:将x减到0的最小操作数

        1658. 将 x 减到 0 的最小操作数

        逆向思维:只能从左或者右减去,所以最后的答案必然是原数组的一个子数组。所以问题转换成找到原数组的一个子数组的和为x,求子数组最大长度。

        accmulate:(pos,pos,x)x为初始累加值。

class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        int tar = accumulate(nums.begin(),nums.end(),0) - x;
        if(tar<0) return -1;
        int ans = -1, n = nums.size(),sum = 0;
        for(int i = 0,j = 0;j<n;j++)
        {
            sum+=nums[j];
            while(sum>tar) 
                sum-=nums[i++];
            if(sum == tar) ans = max(ans,j-i+1);
        }
        return ans<0?-1:n-ans;
    }
};

力扣2762:不间断子数组

        2762. 不间断子数组

        分析完题目其实就是记录三个值:集合中的最大值,最小值,跟即将加进来的数。

        我们可以用multiset(红黑树)去维护。

        每次加入multiset中,由于是自动排序的,最大值就是st.rbegin(),最小值是st.begin()。如果当前这个数的加入让最大值与最小值之差小于2了,那么我们就要开始移动左指针了。直到满足条件即可。

        最后的答案就是区间长度。

        code: 注意: 最后一个元素的指针不是end(),是rbegin()。

        erase可以接受值或者是指针。如果接受的是指针,删除的是该指针指向的值,如果是值,那multise中所有该值都会被删除。

        与单调栈,单调队列的区别:这里的区间内元素顺序不是固定的,所以我们可以用multiset,如果元素顺序固定就要用单调队列了。

class Solution {
public:
    long long continuousSubarrays(vector<int>& nums) {
        long long ans = 0 ;
        multiset<int> st;
        int n = nums.size();
        for(int i = 0,j = 0;j<n;j++)
        {
            st.insert(nums[j]);
            while(*st.rbegin() - *st.begin() > 2)
                st.erase(st.find(nums[i++]));
            ans += j -i+1;
        }
        return ans;
    }
};

一些关于哈希表的补充:

        

        

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值