算法篇:滑动窗口

使用范围

此方法针对的对象是一段连续的区间。

做题模板:

区分子数组/子串、子序列、子集

子数组/子串是原数组中连续的一段区间,要求保持顺序,也要求连续。

子序列是原数组中删除若干元素后剩下的序列,不要求保持顺序,要求连续。

子集是从原来集合中任意选取一些元素,不要求保持顺序,也不要求连续。

举例说明

对于数组 [1, 2, 3, 4]:

子数组:[1,2]、[2,3,4]、[3]、[1,2,3,4]

注意:[1,3] 不是子数组(不连续)

子序列:[1,3]、[2,4]、[1,2,4]、[1,2,3,4]

注意:[2,1] 不是子序列(顺序改变)

子集:{1,3}、{2,4}、{1,2,4}、{1,2,3,4}

注意:{2,1} 和 {1,2} 是同一个子集(顺序无关)

题目实例

长度最小的子数组

209. 长度最小的子数组 - 力扣(LeetCode)

子数组为一段连续的区间,可以考虑用滑动窗口的方法解答这个问题。

解题思路:

该题目需要我们找的是总和大于等于target的最小长度的子序列。根据总和,我们可以定义一个sum变量来实现进窗口,当加到总和大于等于target的时候,可以进行更新结果,最后在出窗口,进行后面数组的判断。

复制代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int left=0,right=0;
        int n=nums.size();
        int sum=0,minlen=INT_MAX;
        for(;right<n;right++)
        {
            sum+=nums[right];
            while(sum>=target)
            {
                minlen=min(minlen,right-left+1);
                sum-=nums[left];
                left++;
            }
        }
        return minlen==INT_MAX?0:minlen;
    }
};

注:时间复杂度:虽然代码是两层循环,但是我们的 left 指针和 right 指针都是不回退的,两者

最多都往后移动 n 次。因此时间复杂度是 O(N) 。

无重复字符的最长子串

3. 无重复字符的最长子串 - 力扣(LeetCode)

子串为一段连续的区间,可以考虑用滑动窗口的方法解答这个问题。

解题思路:哈希表+滑动窗口(记得维护哈希表)

右端元素进入窗口时,用哈希表统计元素个数;根据题意:不含重复的字符,这表明这段连续区间内每个元素都只有一个,那么我们就可以得到我们出窗口的条件:只要有一个元素在这段区间内超过1了就对该区间进行出窗口,直到该区间内无重复元素时对它进行更新结果。

复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int hash[128]={0};
        int left=0,right=0;
        int maxlen=0;
        for(;right<s.size();right++)
        {
            char ch=s[right];
            hash[ch]++;
            while(hash[ch]>1)
            {
                char ret=s[left];
                hash[ret]--;
                left++;
            }
            maxlen=max(maxlen,right-left+1);
        }
        return maxlen;
    }
};

当只有小写字母或者大写字母时,哈希表只用hash[26];

当不止一种且都是ASCII中的元素时,哈希表用hash[128]。

最大连续 1 的个数 III

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

根据题意:求取在一段区间内把里面的k个0变成1后,数组中连续1的最大个数,即求取连续区间内最大个数的1+k的最大个数。

解题思路:

用一个变量count来统计0在区间内出现的个数,根据题目:最多可以反转k个0,可以得出出窗口的条件:count>k,在出窗口的时候记得维护count变量。出窗口后更新结果。

复制代码
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int count=0;
        int left=0,right=0;
        int maxnum=0;
        for(;right<nums.size();right++)
        {
            //for循环中right++已经是进窗口了
            if(nums[right]==0)
            {
                count++;
            }
            //出窗口
            while(count>k)
            {
                if(nums[left]==0)
                {
                    count--;
                }
                left++;
            }
            //更新结果
            maxnum=max(maxnum,right-left+1);
        }
        return maxnum;
    }
};

将 x 减到 0 的最小操作数

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

题意:移除最左边或最右边的数,使移除的数等于x

转换思维:题目中移除的是最右边或最左边的数,是不是只要保证中间的区域只要等于全部数的总和-x就行了,所以我们就转换求结果等于sum-x的最长子数组,最后用总长减最长子数组即可。

解题思路:

根据思路,我们可以在数组中找一段连续的区间等于sum-x即可,解题方法如第一道题一样,相当于这个题目的target=sum-x,但是更新结果和出窗口的位置不一样。这题是要在总和等于sum-x的时候更新结果,而出窗口的提交条件是区间中的和加起来大于target,所以不能在出窗口的时候更新结果,出完窗口后,我们才能更新结果。

细节问题:这里我们求的是最长的子数组,但是定义最长子数组长度的值不能初始化为0,因为当数组中全部数都大于x的时候,最长子数组的长度为0,而整个数组的和为x的时候,最长子数组长度的和也为0,但是第一种输出的结果为-1,第二个输出的是数组的长度。

复制代码
class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        //求取中间连续区间的值等于总和-x
        long long sum=0;
        for(auto &s:nums)
        {
            sum+=s;
        }
        if(sum<x)
        {
            return -1;
        }
        int left=0,right=0;
        int maxnum=-1;
        long long  ret=0;
        long long target=sum-x;
        for(;right<nums.size();right++)
        {
            ret+=nums[right];
            while(ret>target)
            {
                //出窗口
                ret-=nums[left];
                left++;
            }
            if(ret==target)
            maxnum=max(maxnum,right-left+1);
        }
        return maxnum==-1?-1:nums.size()-maxnum;
    }
};

水果成篮

904. 水果成篮 - 力扣(LeetCode)

题意解析:一共有两个篮子,每个篮子只能放同一种水果(也就是数组中数字一样的)例如:

fruits = [1,2,3,2,2] 这个数组中有三种水果,分别是1,2,3.

解题思路:哈希表+滑动窗口

进窗口:把右端的值放入到哈希表中,哈希表中统计每个水果的个数。

出窗口:题目中的限制条件是两个篮子,那么两个篮子就可以设置成出窗口的条件。

更新结果:更新最大数目

哈希表可以用两种形式的:unordered_map<int,int>hash,int hash[100001]={0};

如果用unordered_map这种容器的哈希表,记得元素的个数为0的时候要删除元素。

复制代码
int totalFruit(vector<int>& fruits) {
        unordered_map<int,int>hash;
        int left=0,right=0,n=fruits.size();
        int maxlen=-1;
        for(;right<n;right++)
        {
            hash[fruits[right]]++;
            while(hash.size()>2)
            {
                //出窗口
                hash[fruits[left]]--;
                if(hash[fruits[left]]==0)
                {
                    hash.erase(fruits[left]);
                }
                left++;
                
            }
            maxlen=max(maxlen,right-left+1);
        }
        return maxlen;
    }
};

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int hash[100001]={0};
        int left=0,right=0,n=fruits.size();
        int maxlen=-1;
        int count=0;//水果的种类
        for(;right<n;right++)
        {
            if(hash[fruits[right]]++==0)
            {
                count++;
            }
            while(count>2)
            {
                //出窗口
                hash[fruits[left]]--;
                if(hash[fruits[left]]==0)
                {
                    count--;
                }
                left++;
                
            }
            maxlen=max(maxlen,right-left+1);
        }
        return maxlen;
    }
};

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

438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

求的是子串,子串是一段连续的区间,可以考虑滑动窗口。

解题思路:哈希+滑动

定义一个变量来统计在s中有多少符合p个数的字符串count,还定义两个哈希表,一个哈希表hash1来统计p中所有个数,一个哈希表hash2来统计s中所有个数,这三个变量都是在接下来的解题中需要维护的。

进窗口:把s中的字符统计的同时,维护count变量,统计有多少个符合p个数的字符串,可以通过hash2[s[right]]<=hash1[s[right]]统计。

出窗口:如果用count>right-left+1来作为出窗口的条件的话,那么就只是表明在这个数组中right-left+1这一段里面没有连续的符合p的子串,而且这个条件也不能判断count是不是等于p.size(),所以这个不能作为判断条件。判断要和p的大小有关,而且要符合长度,所以用right-left+1>p.size()作为判断条件。出窗口时我们要维护count变量,hash2变量和left变量。

更新结果:判断条件:count==p.size()

复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        //哈希+滑动
        unordered_map<char,int>hash1;
        unordered_map<char,int>hash2;
        vector<int >result;
        for(auto &s:p)
        {
            hash1[s]++;
        }
        int left=0,right=0;
        int count=0;//符合的个数
        for(;right<s.size();right++)
        {
            char ch=s[right];
            if(++hash2[ch]<=hash1[ch])count++;
            while(right-left+1>p.size())
            {
                //出窗口
                char i=s[left];
                if(hash2[i]--<=hash1[i])count--;
                left++;
            }
            if(count==p.size())
            {
                result.push_back(left);
            }
        }
        return result;
    }
};

串联所有单词的子串

30. 串联所有单词的子串 - 力扣(LeetCode)

题意解析:找到数组中一段包含words中所有单词的连续区间,单词的顺序可以任意。连续区间,可以考虑用滑动窗口。

关键信息:words中所有字符串长度一样。

解题思路:

把数组分割成words中单词长度的大小,但是我们怎么知道要从哪里开始分割才能遇到words中的单词呢?这时候我们就需要一个循环,这里可以优化:但我们循环超过words字符串长度一样的时候,发现和从0开始遍历的单词是一样的了,所以我们可以缩小循环次数在单词长度之间。

这题上题解题类似,只是把字符看成字符串就行。

细节部分:count的变量应该放在循环里,遍历字符串s的哈希表也要放在循环里,不然哈希表每次遍历都不是新的结果。

复制代码
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        unordered_map<string, int> hash1;
        
        vector<int> result;
        for (auto& s : words) {
            hash1[s]++;
        }
        int left = 0, right = 0;
        int m = words[0].size();
        
        for (int i = 0; i < m; i++) {
            int count = 0;
            unordered_map<string, int> hash2;
            for (right=i,left=i; right < s.size(); right += m) {
                string rs = s.substr(right, m);
                if (++hash2[rs] <= hash1[rs]) {
                    count++;
                }
                while (right - left + 1 > m * words.size()) {
                    string ls = s.substr(left, m);
                    if (hash2[ls]-- <= hash1[ls]) {
                        count--;
                    }
                    left += m;
                }
                if(words.size()==count)
                {
                    result.push_back(left);
                }
            }
        }
        return result;
    }
};

最小覆盖子串

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

题目解析:寻找一段连续区间中只要包含t中所有的字符的最短子串,即最短子串中可以存在其他字符,但是一定要全部包含t中所有的字符。

解题思路:和找到字符串中所有字母异位词的解题思路一样,但是这题要求的是返回字符串。

进窗口:遍历右端元素进入哈希表

出窗口:根据题意,寻找的是最短的子串,那么也就是说最好最后一个结尾的就是t元素中包含的全部字符的最后一个字符,所以这里的出窗口的条件就是当区间内刚好count==t.size()。这个时候就是刚好符合题目的要求,所以我们要在出窗口前更新结果,而题目返回的是字符串,那么我们可以通过string容器中的substr函数来进行结果返回,sustr有两个参数,一个是起始位置,一个是截取多少个字符,所以我们要定义两个变量来记录。

细节部分:begin设置为-1,记录子串起始位置的值,返回的时候要对begin进行判断。

复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char,int>hash1;
        for(auto&ch:t)
        {
            hash1[ch]++;
        }
        int left=0,right=0;
        int minlen=INT_MAX;
        int count=0;
        int begin=-1;
        unordered_map<char,int>hash2;
        for(;right<s.size();right++)
        {
            if(++hash2[s[right]]<=hash1[s[right]])count++;
            //出窗口
            while(count==t.size())
            {
                if(minlen>right-left+1)
                {
                    minlen=min(minlen,right-left+1);
                    begin=left;
                }
                if(hash2[s[left]]--<=hash1[s[left]])count--;
                left++;
            }
        }
        if(begin==-1)
        return "";
        else
         return s.substr(begin,minlen);
    }
};
相关推荐
无限进步_2 小时前
【C++】单词反转算法详解:原地操作与边界处理
java·开发语言·c++·git·算法·github·visual studio
泯泷2 小时前
从零构建寄存器式 JSVMP:实战教程导读
前端·javascript·算法
NGC_66112 小时前
值传递和引用传递辨析
算法
寒月小酒2 小时前
3.21 OJ
算法·深度优先
Book思议-2 小时前
【数据结构考研真题】链表大题
c语言·数据结构·考研·算法·链表·408·计算机考研
m0_528174452 小时前
ZLibrary反爬机制概述
开发语言·c++·算法
你这个代码我看不懂2 小时前
引用计数法存在的问题
java·jvm·算法
yunyun321233 小时前
嵌入式C++驱动开发
开发语言·c++·算法
Storynone3 小时前
【Day29】LeetCode:62. 不同路径,63. 不同路径 II,343. 整数拆分,96. 不同的二叉搜索树
python·算法·leetcode