保研机试之【滑动窗口】

前言

本篇为滑动窗口算法核心代码模板 | labuladong 的算法笔记的笔记。

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

基于滑动窗口算法框架写出的代码,时间复杂度是 O(N),比嵌套 for 循环的暴力解法效率高。

滑动窗口模版

/* 滑动窗口算法框架 */
void slidingWindow(string s) {
    // 用合适的数据结构记录窗口中的数据,根据具体场景变通
    // 比如说,我想记录窗口中元素出现的次数,就用 map
    // 我想记录窗口中的元素和,就用 int
    unordered_map<char, int> window;
    
    int left = 0, right = 0;
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        window.add(c)
        // 增大窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        // 注意在最终的解法代码中不要 print
        // 因为 IO 操作很耗时,可能导致超时
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (left < right && window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            window.remove(d)
            // 缩小窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

六道题讲解模版

第一题

76. 最小覆盖子串 - 力扣(LeetCode)

3b3244ac3aa94d6cb623f6ab14f4cb15.png

cded11869b27422c80cb43f81397baf3.png
一开始我的想法是这样的,但是会TLE(我的判断函数是针对字符串t中的每个元素,比对hmap_tt和hmap_s中的值):

class Solution {
public:

    bool judge(unordered_map<char,int> hmap_s,unordered_map<char,int> hmap_tt,string t,int len_tt){
        for(int i=0;i<len_tt;i++){
            if(hmap_tt[t[i]]>hmap_s[t[i]]){
                return false;
            }
        }
        return true;
    }
    string minWindow(string s, string t) {
        unordered_map<char,int> hmap_s;
        unordered_map<char,int> hmap_tt;
        string res="";
        int res_len=INT_MAX;
        int len_s=s.size();
        int len_tt=t.size();
        int l=0;
        int r=0;
        int res_l=0;
        int res_r=0;

        for(int i=0;i<len_tt;i++){
            hmap_tt[t[i]]++;
        }

        while(r<len_s){
            char temp=s[r];
            r++;
            hmap_s[temp]++;
            while(l<=r && judge(hmap_s,hmap_tt,t,len_tt)){
                if(res_len>r-l){
                    res_len=r-l;
                    res_l=l;
                    res_r=r;
                }
                char cnt=s[l];
                l++;
                hmap_s[cnt]--;
            }
        }

        if(res_len==INT_MAX){return "";}
        return ""+s.substr(res_l,res_len);
    }
};

但是,我们可以使用备忘录:valid 变量表示窗口中满足 need 条件的字符个数,如果 valid 和 need.size 的大小相同,则说明窗口已满足条件。修改代码如下:

class Solution {
public:

    bool judge(int valid,int len_tt){
        if(valid>=len_tt)
            return true;
        else
            return false;
    }
    string minWindow(string s, string t) {
        unordered_map<char,int> hmap_s;
        unordered_map<char,int> hmap_tt;
        string res="";
        int res_len=INT_MAX;
        int len_s=s.size();
        int len_tt=t.size();
        int l=0;
        int r=0;
        int res_l=0;
        int res_r=0;
        int valid=0;

        for(int i=0;i<len_tt;i++){
            hmap_tt[t[i]]++;
        }

        while(r<len_s){
            char temp=s[r];
            r++;
            hmap_s[temp]++;
            if(hmap_s[temp]<=hmap_tt[temp]){valid++;}
            while(l<=r && judge(valid,len_tt)){
                if(res_len>r-l){
                    res_len=r-l;
                    res_l=l;
                    res_r=r;
                }
                char cnt=s[l];
                l++;
                hmap_s[cnt]--;
                if(hmap_s[cnt]<hmap_tt[cnt]){valid--;}
            }
        }

        if(res_len==INT_MAX){return "";}
        return ""+s.substr(res_l,res_len);
    }
};

第二题

1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)

6703dd48904c4f38bf51af9846b8107d.png

5ddd4638790b491881ba61b9e936c0e7.png

这道题该怎么应用滑动窗口呢(题目说了 nums 中的元素都是正数,这就保证了只要有元素加入窗口,和一定变大,只要有元素离开窗口,和一定变小;负数的话就没有这个性质了,也就不能确定什么时候扩大和缩小窗口,也就不能使用滑动窗口算法,那应该使用什么方法呢?前缀和+哈希,后面会进行讨论

题目让你从边缘删除掉和为 x 的元素,那剩下来的是什么?剩下来的是不是就是 nums 中的一个子数组?让你尽可能少地从边缘删除元素说明什么?是不是就是说剩下来的这个子数组大小尽可能的大? 这道题等价于让你寻找 nums 中元素和为 sum(nums) - x 的最长子数组

代码如下:

class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        int len=nums.size();
        int sum=0;
        for(int i=0;i<len;i++){sum+=nums[i];}
        int temp_sum=sum;
        sum-=x;
        int l=0;int r=0;int cnt=0;int res=0;
        while(r<len){
            int temp=nums[r];
            r++;
            cnt+=temp;
            while(l<r && cnt>=sum){
                int temp_l=nums[l];
                if(r-l>res && cnt==sum){res=r-l;}
                l++;
                cnt-=temp_l;
            }
        }
        res=len-res;
        if(res==len && temp_sum!=x ){return -1;}
        return res;
    }
};

现在讨论元素存在负数的情况:

首先先来看一下这道题(采用前缀和+哈希的方式来解决):560. 和为 K 的子数组 - 力扣(LeetCode)

5fac37dd08a647519ea9bc63c8016aaa.png

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        int len=nums.size();
        //对于数组中的每一个元素x,只要在哈希表中,包含x的前缀和y-k存在,即这种情况成立
        vector<int> g(len+1,0);
        unordered_map<int,int> hmap;
        int res=0;
        g[0]=0;
        hmap[0]=1;
        for(int i=0;i<len;i++){
            g[i+1]=nums[i]+g[i];
            if(hmap.find(g[i+1]-k)!=hmap.end()){
                res+=hmap[g[i+1]-k];
            }
            hmap[g[i+1]]++;
        }
        return res;
    }
};

现在再尝试用哈希和前缀和的思想来解决第二题:

class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        int len=nums.size();
        int sum=0;
        for(int i=0;i<len;i++){sum+=nums[i];}
        int temp_sum=sum;
        sum-=x;
        vector<int> g(len+1,0);
        unordered_map<int,int> hmap;
        hmap[0]=0;
        int res=0;
        for(int i=0;i<len;i++){
            g[i+1]=g[i]+nums[i];
            if(hmap.find(g[i+1]-sum)!=hmap.end()){
                res=max(res,i+1-hmap[g[i+1]-sum]);
            }
            if(hmap.find(g[i+1])==hmap.end()){
                hmap[g[i+1]]=i+1;
            }
        }
        if(res==0 && sum!=0){return -1;}
        return len-res;
    }
};

不得不想到软微笔试第二题:给定数组A,数组长度N,以及数S,求得满足和>S的子数组最小长度,需要满足小于O(NlogN)的时间复杂度。前缀和+哈希表无法解决这道题是因为满足和>S而非=S。【需要拓展怎么求解】

第三题

1004. 最大连续1的个数 III - 力扣(LeetCode)

在可以修改字符的条件下寻找符合条件的子数组,也可以用滑动窗口算法

b33f6dee87bc435b899ba3f59e873793.png

代码如下:

class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int len=nums.size();
        vector<int> g;
        g=nums;
        int l=0;int r=0;int res=0;int temp=0;
        while(r<len){
            if(nums[r]==0 && k>0){
                nums[r]=1;
                k--;
                temp++;
            }
            else if(nums[r]==1){
                temp++;
            }
            res=max(res,temp);
            r++;
            if(r<len && r-1>=0 && nums[r-1]==1 && nums[r]==1){continue;}
            while(l<r && k==0){
                if(g[l]==0 && nums[l]==1){
                    nums[l]=0;
                    k++;
                }
                if(temp>0){temp--;}
                l++;
            }
        }
        return res;
    }
};

第四题

进阶版的第三题:424. 替换后的最长重复字符 - 力扣(LeetCode)

2949d5ad24804b3284771053314bd483.png

要处理很多的小细节,循环26个字母 i ,依次判断替换成字母 i 后(有 k 次机会)的最长重复字符长度。

代码如下:

class Solution {
public:
    int characterReplacement(string s, int k) {
        int len=s.size();
        string ss=s;
        int res=0;
        char standard;
        int l,r,temp_res,temp_k;
        for(int i=0;i<26;i++){
            standard=i+'A';
            l=0,r=0;
            temp_res=0;
            temp_k=k;
            while(r<len){
                char temp=s[r];
                if(temp!=standard && temp_k>0){
                    s[r]=standard;
                    temp_k--;
                    
                }
                r++;
                if(r<len && s[r]==standard && s[r-1]==standard){res=max(res,r-l+1);continue;}
                while(l<r && temp_k==0){
                    res=max(res,r-l);
                    char num=s[l];
                    if(num!=ss[l]){
                        s[l]=ss[l];
                        temp_k++;
                    }
                    l++;
                }
            }
            s=ss;
        }
        return res;
    }
};

第五题

219. 存在重复元素 II - 力扣(LeetCode)

590376e0f2db48319d1b7e0f798624c7.png

在指定大小的子数组中寻找符合条件的元素,也可以用到滑动窗口算法。

这题考察滑动窗口技巧,你维护一个大小为 k 的滑动窗口滑过整个数组,滑动的过程中计算窗口中是否存在重复元素。

class Solution {
public:
    bool containsNearbyDuplicate(vector<int>& nums, int k) {
        int len=nums.size();
        k=min(len-1,k);
        int l=0,r=k;
        unordered_set<int> hset;
        for(int i=0;i<k;i++){
            if(hset.find(nums[i])!=hset.end()){return true;}
            hset.insert(nums[i]);
        }
        while(r<len){
            int temp=nums[r];
            if(hset.find(temp)!=hset.end()){return true;}
            else{
                hset.insert(temp);
            }
            r++;
            if(l<r){
                hset.erase(nums[l]);
                l++;
            }
        }
        return false;
    }
};

第六题

220. 存在重复元素 III - 力扣(LeetCode)

  • abs(i - j) <= indexDiff 滑动窗口信号
  • abs(nums[i] - nums[j]) <= valueDiff 数据结构(set)维护滑动窗口内的元素:
    • 支持添加和删除指定元素的操作,否则我们无法维护滑动窗口;

    • 内部元素有序,支持二分查找的操作(lower_bound函数),这样我们可以快速判断滑动窗口中是否存在元素满足条件。

class Solution {
public:
    bool containsNearbyAlmostDuplicate(vector<int>& nums, int indexDiff, int valueDiff) {
        int len=nums.size();
        int l=0,r=0;
        set<long> s;
        while(r<len){
            int temp=nums[r];

            auto find=s.lower_bound(long(temp));
            if(find!=s.end() && *find-temp<=valueDiff){return true;}
            if(find!=s.begin()){
                find--;
                if(temp-*find<=valueDiff){return true;}
            }
            
            s.insert(temp);
            r++;

            while(l<r && r-l>indexDiff){
                s.erase(nums[l]);
                l++;
            }
        }
        return false;
    }
};

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值