【算法】滑动窗口

滑动窗口是基于双"指针"的一种思想,两个指针指向的元素之间形成一个窗口,窗口有两类,一种是固定大小类的窗口,一类是大小动态变化的窗口。

对于滑动窗口的概念我们暂且理解到这,接下来我们结合具体题目来更深入理解滑动窗口。

一、长度最小的子数组

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

给定一个含有 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

提示:

  • 1 <= target <= 10^9
  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^5

方法一:暴力枚举法,将所有子数组一一枚举出来,先看总和是否大于target,再决定是否更新最短长度。

图示如下(以示例1为例):

暴力枚举法将所有情况一一列举出来,这是一定能找出最短长度。

代码实现(C++,时间复杂度O(N^2)):

//方法一:暴力枚举法
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n=nums.size(); //记录数组长度
        int len = INT_MAX; //记录最短长度,初始值为INT_MAX,方便后面用min函数
       
        for(int left=0;left<n;left++)
        {
            int sum=0; //记录left到right区间所有元素的和
            int right = left; //right的起始指向left
            while(right < n)
            {
                sum+=nums[right];
                if(sum >= target)
                    len = min(len,right-left+1);//获取最小值
                ++right; //更新right,直到不满足while循环条件
            }
        }
        //当两层循环走完后,此时的len就是最小长度
        return len == INT_MAX ? 0 : len;
    }
};

 运行结果:

数据量越大,这种暴力枚举的数量就越多,所以就会出现超时的情况。

我们能不能在此基础上大量减少枚举的数量?分析如下(以示例1为例):

代码实现(C++,时间复杂度O(N)): 

//方法二:滑动窗口
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n=nums.size();
        int len = INT_MAX;
        int sum = 0;
        for(int left = 0,right = 0; right < n; ++right)
        {
            sum+=nums[right]; //进窗口,记录left到right区间所有数的和
            while(sum >= target) //判断
            {
                len = min(len,right-left+1);//更新最短长度
                sum-=nums[left++]; //出窗口,并更新left,更新后sum的值还可能大于等于target,所以判断是一个循环
            }
        }
        return len == INT_MAX ? 0 : len;
    }
};

代码中虽然是两层循环,但是结合实例,我们的left指针和right指针都是不回退的,两者最多都往后移动n次,因此时间复杂度是O(N)。相比于暴力解法,这种优化的方法效率更高。 

二、无重复字符的最长字串

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

给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 10^4
  • s 由英文字母、数字、符号和空格组成

方法一:暴力枚举法,分析如下(以示例1为例):

在往后寻找无重复子串能到达的位置时,可以利用「哈希表」统计出字符出现的频次,来判断什么
时候子串出现了重复元素。

代码实现(C++,时间复杂度O(N^2)):

//方法一:暴力求解
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int len = 0; //记录符合要求的最长长度,初始值为0,方便后面使用max
        int n = s.size();//记录串的长度
        for(int left = 0;left<n;++left)
        {
            int hash[130] = {0}; //所有字符种类加起来不会超过130个,用来统计字符出现的频次
            for(int right = left;right<n;++right) //right每次起始位置都是left
            {
                hash[s[right]]++;  //统计right位置的字符的频次
                if(hash[s[right]] > 1) //说明right此时指向的字符与前面的字符有重复
                    break;             //有重复就不要继续向后移动right了
                
                //走到这里说明没有重复,那就更新len
                len = max(len,right-left+1);
            }
        }
        //两层循环走完后,此时的len就是结果
        //return len == 0 ? 0 : len;
        return len; 
    }
};

这道题暴力求解可以通过,但是在此基础上还有优化的空间,就是利用滑动窗口,分析如下(以示例1为例):

代码实现(C++,时间复杂度O(N)): 

//方法二:滑动窗口
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n = s.size(); //记录字符串长度
        int len = 0;//记录最长字符串的长度
        int hash[130] = {0};//用数组来模拟哈希表
        for(int left = 0,right = 0; right < n; ++right)
        { 
            //1、进窗口
            hash[s[right]]++;
            //2、判断窗口内是否有重复字符
            while(hash[s[right]] > 1)
                hash[s[left++]]--; //3、出窗口,hash表中left对应的字符次数减1
            //4、更新
            len = max(len, right-left+1);
        }
        return len;
    }
};

代码中虽然是两层循环,但是结合实例,我们的left指针和right指针都是不回退的,两者最多都往后移动n次,因此时间复杂度是O(N)。相比于暴力解法,这种优化的方法效率更高。  

三、 最大连续1的个数 III

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

给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 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。

提示:

  • 1 <= nums.length <= 10^5
  • nums[i] 不是 0 就是 1
  • 0 <= k <= nums.length

题目中说可以翻转最多k个0,意思就是我们可以在0、1、2...k中任意选取一个数作为翻转次数。(翻转就是指将数组中的0变为1)

同样的,我们先从暴力解法入手,如果直接用暴力解法那将会是非常麻烦,比如枚举一次情况后,我们还需将这次情况中由0变为1的数,全部重新改为0,然后再来枚举下一次情况。 代码实现起来将会特别复杂,所以我们可以换一个角度来思考问题:

只需在这个数组中找到一个连续的区域,使这个区域中0的个数小于等于k。

进而将问题转化成:在数组中找出最长的子数组,在这个子数组中0的个数不超过k个。

方法一:暴力枚举法。将数组中所有符合要求的子数组枚举出来,符合要求就是指:数组中0的个数不超过k个。

分析如下(以示例1为例):

使用一个计数器(zero)就可以判断0的个数与K之间的关系。

代码实现(C++,时间复杂度O(N^2)):

//方法一:暴力枚举法
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int n = nums.size();//数组元素个数
        int len = 0; //记录最大长度
        for(int left = 0;left<n;++left)
        {
            int zero = 0; //计数器,记录left到right之间0的个数
            for(int right=left;right<n;++right) //left固定位置改变就需要更新right
            {
                if(nums[right] == 0)
                    ++zero;  //当前位置为0,计数器就++
                if(zero > k) //当计数器大于k,就更新len
                {
                    len = max(len,right-left);
                    break; //跳出循环,这趟已经结束,然后更新left
                }
                if(right == n - 1) //right走到数组最后一个元素的位置,但zero<=k,这时就需要单独处理更新len
                    len = max(len,right-left+1);
            }
        }
        return len;
    }
};

当数据量很大,那么这种暴力方法就会超时:

我们在此基础上可以对暴力求解进行优化,分析如下:

这里也需使用一个计数器(zero)来判断0的个数与K之间的关系。 

代码实现(C++,时间复杂度O(N)):

//方法二:滑动窗口
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int n=nums.size();//记录数组个数
        int len = 0;//记录最大长度(个数)
        int zero = 0; //记录left到right中0的个数
        for(int left = 0,right = 0;right<n;++right)
        {   
            //入窗口
            if(nums[right] == 0) 
                ++zero;    
            //判断
            while(zero > k)
            {
                if(nums[left++] == 0) //出窗口,直到zero <= right
                    --zero;   //如果满足条件就更新zero
            }
            //更新len
            len = max(len,right-left+1); 
        }
        return len;
    }
};

代码中虽然是两层循环,但是结合实例,我们的left指针和right指针都是不回退的,两者最多都往后移动n次,因此时间复杂度是O(N)。相比于暴力解法,这种优化的方法效率更高。   

以上三个题中的窗口大小都不是固定的。

未完待续...

上一篇:阿里云NAS之间迁移实践


下一篇:网络安全的全面指南