例题
给定一个含有 n
个正整数的数组和一个正整数 target
。找出该数组中满足其和 ≥ target
的长度最小的连续子数组 [numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回 0
。
输入:s = 7,nums= [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
滑动窗口
-
不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
- 先移动终点使子数组符合题意,即找到可行解;
- 然后移动起点使子数组长度尽量小,即寻找最优解。
-
在本题中实现滑动窗口,主要确定如下三点:
-
窗口内是什么?
- 满足其和
≥s
的长度最小的连续子数组。
- 满足其和
-
如何移动窗口的起始位置?
- 如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
-
如何移动窗口的结束位置?
- 窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
// 时间复杂度:O(n) // 空间复杂度:O(1) class Solution { public: int minSubArrayLen(int target, vector<int>& nums) { // 滑动窗口(方法一) int minLen=nums.size()+1; int curLen; int sum=0; // i是起点 int i=0; // j是终点 for(int j=0;j<nums.size();j++) { sum+=nums[j]; while(sum>=target) { curLen=j-i+1; minLen=curLen<minLen?curLen:minLen; sum-=nums[i++]; } } if(minLen==nums.size()+1) return 0; return minLen; };
注意:虽然
for
循环里面有while
,但是主要看每个元素的操作次数,每个元素都是在滑动窗口进去操作一次,出来操作一次,每个元素都是被操作两次,所以时间复杂度2*n
,也就是O(n)
-
刷题
904. 水果成篮
找至多包含两种元素的最长子串,返回其长度。
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘 [1,2,1,1,2] 这五棵树。
-
例题是在迭代右移左边界的过程中更新结果,而本题是在迭代右移右边界的过程中更新结果。
-
数据结构:利用哈希表来存储这个窗口内的数以及出现的次数。
-
我们每次将
j
向右移动一个位置,并且将fruits[j]
加入哈希表。如果哈希表大小大于2
,那就移动i
将fruits[i]
从哈希表中移除,直到满足要求为止。 -
如果
fruits[i]
在哈希表中出现的次数减少为0
,那么要将其值移出,此时哈希表大小为2
,继续循环。
-
class Solution {
public:
int totalFruit(vector<int>& fruits) {
// 滑动窗口
int i=0;
int maxLen=0;
int curLen;
unordered_map<int,int> cnt;
for(int j=0;j<fruits.size();j++)
{
cnt[fruits[j]]++;
while(cnt.size()>2)
{
cnt[fruits[i]]--;
if(cnt[fruits[i]]==0)
cnt.erase(fruits[i]);
i++;
}
curLen=j-i+1;
maxLen=curLen>maxLen?curLen:maxLen;
}
return maxLen;
}
};
76. 最小覆盖子串
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
- 这里采用一个哈希表的方式来表示滑动窗口内子串还需要的字符信息,若哈希表中所有值均小于等于
0
,则说明符合题意。 - 关于数据结构:其实本题、上一题与例题最大的区别就是:例题中题目要求子数组元素和大于等于目标值,这是可以直接用一个变量来表示的;然而上一题要求子串中元素种类不多于
2
,和本题要求元素能覆盖t
串中元素,这就是一个变量无法表示的,所以才用哈希表。
class Solution {
public:
string minWindow(string s, string t) {
// 初始化i,j分别为滑动窗口的起点和终点
// 定义符合题意的子串的起点
int i=0,j=0,mini;
// 定义当前的长度和最小字符串长度
int curLen,minLen=s.length()+1;
// 初始化一个空字符串
string result="";
// 哈希表用来表示还需要的字符
unordered_map<char,int> cnt;
// 先用字符串t需要的字符及其数量来初始化哈希表
for(int k=0;k<t.length();k++)
{
cnt[t[k]]++;
}
// 对终点j进行遍历
for(;j<s.length();j++)
{
// 如果终点指向的字符在哈希表中存在,则将哈希表中它的值减一
// 即终点字符被包含在了滑动窗口中,所以还需要的字符数量减一
if(cnt.find(s[j])!=cnt.end())
cnt[s[j]]--;
// isValid函数用于判断滑动窗口内的字符串是否满足题意
// 即判断所有还需要的字符数量是否全小于等于0
// 只要子串还是符合题意的,在进入while循环后更新最小子串
while(isValid(cnt))
{
// 计算当前子串长度
curLen=j-i+1;
// 如果比最小值还小,就更新最小值和起点i的值
// 注意:我最开始在这里直接更新字符串result,内存超了
// 所以我们只需要记录起点,循环结束后再更新result即可
if(curLen<minLen)
{
minLen=curLen;
mini=i;
}
// 如果起点元素是t中元素,则在哈希表中将其值加一
// 因为起点右移后,这个元素将不被滑动窗口包含
// 当然也不是说就一定缺这个元素,因为它在哈希表中的值可能是负的
if(cnt.find(s[i])!=cnt.end())
cnt[s[i++]]++;
else
i++;
}
}
// 如果不存在这样的子串,则最小长度仍为初始值,此时返回空串
// 否则返回起点为mini,长度为minLen的子串
if(minLen!=s.length()+1)
result=s.substr(mini,minLen);
return result;
}
// 判断哈希表的值是否全小于等于0
bool isValid(const unordered_map<char, int>& hashMap)
{
for (const auto& pair : hashMap)
{
if(pair.second>0)
return false;
}
return true;
}
};