【LeetCode热题100】滑动窗口

这篇博客总结了滑动窗口的8道常见题目,分别是:长度最小的子数组、无重复字符的最长子串、 最大连续1的个数III、将x减到0的最小操作数、水果成篮、找到字符串中所有字母异位词、串联所有单词的子串、最小覆盖子串。

cpp 复制代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) 
    {
        int len = INT_MAX,left=0,right=0,sum=0;
        int n = nums.size();
        for(int left=0,right=0;right<n;right++)
        {
            sum += nums[right];//进窗口
            while(sum >= target)//判断
            {
                len = min(len,right-left+1);
                sum -= nums[left++];//出窗口
            }
        }

        return len==INT_MAX?0:len;
    }
};

题目分析:这道题有两种解法:暴力解法和优雅解法。首先我们先从暴力解法说起,暴力枚举出所有子数组的和,定义left和right都指向数组第一个元素,求出left和right之间元素的和,然后固定left,right++,sum加上right指向的元素,然后继续right++,sum再加上right指向的元素,当sum>=taregt时,算出这时left和right之间元素个数,然后left继续右移,直到找到结尾,然后left++,继续right从此时的left开始继续往右走,直到找到所有的结果。

但是,暴力解法有很多可改进的区间,首先,当我们找到第一个sum>=taregt时的right,接着right后面的就不用遍历了,肯定不符合条件。然后,当left++后,此时我们不用把right移动到left的位置,只需要让之前的sum减去left上一个位置的值。利用这两点,我们的优雅解法的思路就是,利用单调性,使用"同向双指针"来优化,left和right都只向同一个方向移动,都不回退。同向双指针又称滑动窗口,滑动窗口用来维护区间的和,当两个指针都不回退时,就可以用滑动窗口。

那怎么用滑动窗口呢?1.设置左右窗口left=0和right=0 2.进窗口 3.根据进的窗口判断是否出窗口,循环23步。

滑动窗口的时间复杂度是O(N)。

cpp 复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) 
    {
        int hash[128] = {0};
        int ret = 0;
        int n = s.size();
        for(int left = 0,right = 0;right<n;right++)
        {
            hash[s[right]]++;//进窗口
            while(hash[s[right]] == 2)//判断
                hash[s[left++]]--;//出窗口
            ret = max(ret,right-left+1);//更新
        }
        return ret;

    }
};

题目分析:开始时,我们设置left和right都指向字符串的开始,然后right向右走,直到找到重复的字符串,然后让left++,一直跳过重复的字符串(使用哈希表判断有无重复字符),此时,我们还需要让right退回和left一样的位置重新遍历吗?不需要,因为left和right中间的肯定没有重复字符串,也就是说,在遍历的过程中,left和right都不需要回退,那么我们就可以使用滑动窗口的思想解决:

1.left=0,right=0

2.进窗口

3.判断

出窗口

4.更新

cpp 复制代码
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) 
    {
        int cnt = 0;
        int size = nums.size();
        int len = 0;
        for(int left = 0,right = 0;right < size; right++)
        {
            if(nums[right] == 0)//进窗口
            {
                cnt++;
            }
            while(cnt > k)//判断
            {
                if(nums[left] == 0)
                    cnt--;
                left++;//出窗口
            }
            len = max(len,right-left+1);
        }

        return len;

    }
};

题目分析:题目可以转化为找出最长的子数组,其0的个数不超过k个,题目也是有两种解法:暴力解法和优雅解法。

暴力解法:暴力枚举每一个子数组,同时加上zero计数器(一个变量),通过判断zero的大小来判断这是不是符合要求的子数组。

优雅解法:left和right指向数组的开始,right向后遍历,当子数组中的0的个数为k+1时,left++,直到其间子数组0的个数为k,然后right继续向后++,直到最后。我们发现right和left都不需要回退,因此可以使用滑动窗口来解决。1.left = 0,right=0;2.进窗口 3.判断-出窗口-更新结果。

cpp 复制代码
class Solution {
public:
    int minOperations(vector<int>& nums, int x) 
    {
        long long s = 0;
        for(auto e:nums) s+=e;
        int target = s-x;
        if(target < 0) return -1;
        else if(target == 0) return nums.size();
        int len = 0;
        int n = nums.size();
        int sum = 0;
        for(int left = 0,right = 0;right<n;right++)
        {
            //1.进窗口
            sum += nums[right];
            //2.判断
            while(sum > target)
            {
                //出窗口
                sum -= nums[left++];
            }
            if(sum == target)
            {
                len = max(len,right-left+1);
            }
                
        }
        return len == 0?-1:n-len;
    }
};

题目分析 :这道题可以转化为找出最长的子数组的长度,所有元素的和正好等于sum-x。经过前面几道题的学习,我们可以明显感觉到可以用滑动窗口的思路解决。1.left=0,right=0 2.进窗口,sum+nums[right],3.判断,是否窗口之间元素的和大于sum-x,如果是,出窗口,直到和≤sum-x,然后出循环后,继续判断是否和等于sum-x,然后依据情况更新len。

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) 
    {
        //map<int,int> m;
        int hash[100001] = {0};
        int len = 0;
        for(int left = 0,right =0,kinds=0;right<fruits.size();right++)
        {
            if(hash[fruits[right]] == 0) kinds++;
            hash[fruits[right]]++;//进窗口
            while(kinds > 2)//判断
            {
                hash[fruits[left]]--;
                if(hash[fruits[left]] == 0) kinds--;
                left++;
            }
            len = max(len,right-left+1);
        }
        return len;
    }
};

题目分析 :题目可以转化为,找出一个最长的子数组的长度,子数组中不超过两种类型的水果。为了判断子数组的水果种类,可以使用哈希表的思想。这道题经过暴力枚举思考后,也可以使用滑动窗口来解决。1.left=0,right=0 2.进窗口,hash[f[right]]++ 3.判断,left和right之间水果种类是否超了,如果没有超,更新len,如果超了,出窗口。

cpp 复制代码
class Solution {
public:
    bool check(int* cmp,int* target)
    {
        for(int i = 0;i<26;i++)
        {
            if(cmp[i] != target[i]) return false;
        }
        return true;
    }
    vector<int> findAnagrams(string s, string p) 
    {
        int hash_target[26] =  {0};
        int hash_cmp[26] = {0};
        vector<int> ret;
        int count = 0;//窗口区间有效字符个数
        for(auto e:p)
        {
            hash_target[e-'a']++;
        }
        for(int left = 0,right=0;right <s.size();right++)
        {
            hash_cmp[s[right]-'a']++;//进窗口
            if(right - left + 1 > p.size())//判断
            {
                hash_cmp[s[left++]-'a']--;//出窗口
            }
            if(check(hash_cmp,hash_target)) ret.push_back(left);//更新结果
        }
        return ret;
    }
};

题目解析:首先我们先来想一想,给出两个字符串,如何判断它们是不是变位词,其实,只要这两个字符串中每种字符的个数一样即可。因此,我们可以创建两个哈希表,遍历这两个字符串,分别将这两个字符串中的字符放到哈希表中,最后遍历这两个哈希表,判断这两个哈希表中对应字符的个数是否相同。

好了,现在,我们需要在字符串s中,依次遍历出长度等于p长度的子串,然后判断这个子串是不是p的变位词,子串的长度我们一直要维护成p的长度,也就是子串长度一直不变,因此,我们可以采用滑动窗口的思想,但是,与之前滑动窗口所不同的是,这道题的滑动窗口长度固定。

1.left=0,right=0 2.进窗口,hash_cmp[in]++ 3.判断,如果right-left+1>m,那么就需要出窗口hash_target[out]-- 3.更新结果,检查这两个字符串的哈希表是否构成变位词。
在上面,我们比较这两个子串是否构成变位词,是通过比较两个哈希表,其实,我们还可以通过利用变量count来统计窗口中"有效字符"的个数,具体来说,在进窗口后,如果hash_cmp[in]<=hash_target[in],count++;在进窗口前,如果hash_cmp[out]<=hash_target[out],说明要出窗口的是有效字符,count--;然后再判断count==m,如果成立,说明窗口内就是p的变位词,更新结果。代码如下:

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) 
    {
        int hash_target[26] =  {0};
        int hash_cmp[26] = {0};
        vector<int> ret;
        int count = 0;//窗口区间有效字符个数
        for(auto e:p)
        {
            hash_target[e-'a']++;
        }
        for(int left = 0,right=0;right <s.size();right++)
        {
            hash_cmp[s[right]-'a']++;//进窗口
            if(hash_cmp[s[right]-'a'] <= hash_target[s[right]-'a']) count++;
            if(right - left + 1 > p.size())//判断
            {
                if(hash_cmp[s[left]-'a'] <= hash_target[s[left] - 'a'])count--;
                hash_cmp[s[left++]-'a']--;//出窗口
            }
            if(count == p.size()) ret.push_back(left);//更新结果
        }
        return ret;
    }
};
cpp 复制代码
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) 
    {
        unordered_map<string,int> hash_target;
        int size = words[0].size();
        vector<int> ret;
        for(auto& e:words)
        {
            hash_target[e]++;
        }
        for(int i = 0;i < size;i++)//执行size次滑动窗口
        {
            unordered_map<string,int> hash_cmp;//需要定义在这里,每次循环就是新的hash_cmp
            for(int left = i,right = i,count=0;right + size <=s.size();right+=size)
            {
                string in(string(s,right,size));
                hash_cmp[in]++;//进窗口
                if(hash_target.count(in) && hash_cmp[in] <= hash_target[in]) count++;
                if(((right - left)/size+1) > words.size())//判断
                {
                    string out(string(s,left,size));
                    //第一个条件判断如果out不存在,那么就不会执行后面的,就不会把out插入
                    if(hash_target.count(out) && hash_cmp[out] <= hash_target[out]) count--;
                    hash_cmp[out]--;
                    left+=size;
                }
                if(count == words.size()) ret.push_back(left);
            }
        }
        return ret;
    }
};

题目解析:这道题和上面的一道题很类似,我们只需要把s中的几个字符看成一个,和上题不同的是:1.我们需要使用map<string,int>这样的容器,来统计区间所包含的字符串 2.left和right每次移动的步数是字符串的长度 3.除了让left=0和right=0,开始遍历外,还需要依次从left=1和right=1、left=2和right=2遍历(假设每个字符串长度为3)。

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) 
    {
        int hash_target[128];
        int hash_cmp[128];
        int kinds = 0;//统计有效字符有多少种
        size_t len = INT_MAX;
        size_t begin = 0;
        
        for(auto e:t)
        {
            if(hash_target[e] == 0)
            {
                kinds++;
            }
            hash_target[e]++;
        }
        for(int left=0,right=0,count=0;right < s.size();right++)
        {
            hash_cmp[s[right]]++;//进窗口
            if(hash_cmp[s[right]] == hash_target[s[right]]) count++;
            while(count == kinds)//判断
            {
                if(right-left+1 < len) //更新
                {
                    len = right - left +1;
                    begin = left;
                }
                if(hash_cmp[s[left]] == hash_target[s[left]]) count--;
                hash_cmp[s[left++]]--;//出窗口
            }
        }
        return len==INT_MAX?string(""):s.substr(begin,len);

    }
};

题目分析 :在经过暴力枚举分析后,我们发现可以使用暴力枚举和哈希表的方式解决。把t中每个字符依次放到哈希表中得到hash_target,然后暴力枚举s中的子字符串,将子字符串依次放到hash_cmp中,比较这两个哈希表,如果hash_target中每个字符的数量<=hash_cmp中对应的每个字符的数量,就认为这个子字符串符合要求。在经过暴力枚举分析后,我们发现其实也可以用滑动窗口的思想解决,也是需要哈希表配合!步骤:1.left=0,right=0 2.进窗口,hash_cmp[in]++ 3.判断,看当前窗口是否符合要求,如果不符合,继续步骤2,如果符合,则出窗口。

然而,上面比较哈希表的开销比较大,我们再来想一种更优秀的办法来判断当前区间是否符合要求,定义count变量,用于标记有效字符的种类,count的使用方法是,在进窗口之后,当hash_cmp[in]==hash_target[in]时,说明这个字符的要求已经达到,有效字符种类count+1;在出窗口之后,当hash_cmp[out]==hash_target[out]时,说明在出完这个字符后,这个字符就不符合要求了,有效字符种类count-1,这样我们就可以通过判断count和t中字符种类是否相等来确定当前区间是否符合要求。

相关推荐
Swift社区1 小时前
LeetCode - #139 单词拆分
算法·leetcode·职场和发展
Kent_J_Truman2 小时前
greater<>() 、less<>()及运算符 < 重载在排序和堆中的使用
算法
IT 青年2 小时前
数据结构 (1)基本概念和术语
数据结构·算法
Dong雨2 小时前
力扣hot100-->栈/单调栈
算法·leetcode·职场和发展
SoraLuna3 小时前
「Mac玩转仓颉内测版24」基础篇4 - 浮点类型详解
开发语言·算法·macos·cangjie
liujjjiyun3 小时前
小R的随机播放顺序
数据结构·c++·算法
¥ 多多¥3 小时前
c++中mystring运算符重载
开发语言·c++·算法
trueEve4 小时前
SQL,力扣题目1369,获取最近第二次的活动
算法·leetcode·职场和发展
天若有情6734 小时前
c++框架设计展示---提高开发效率!
java·c++·算法
ahadee4 小时前
蓝桥杯每日真题 - 第19天
c语言·vscode·算法·蓝桥杯