滑动窗口是基于双"指针"的一种思想,两个指针指向的元素之间形成一个窗口,窗口有两类,一种是固定大小类的窗口,一类是大小动态变化的窗口。
对于滑动窗口的概念我们暂且理解到这,接下来我们结合具体题目来更深入理解滑动窗口。
一、长度最小的子数组
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)。相比于暴力解法,这种优化的方法效率更高。
以上三个题中的窗口大小都不是固定的。
未完待续...