文章目录
- 算法基本思想
- leetcode 209 长度最小的子数组
- leetcode 904 水果成篮
- leetcode 76 最小覆盖子串
算法基本思想
首先对于滑动窗口,题目可以先去看看leetcode 209 进行相关的了解后,再来书写代码。
首先我们的第一想法肯定就是暴力解法:也就是采用两层循环进行遍历,寻找相关的运算并得到最后的答案。下面是暴力的解法:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX; // int的最大值
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
sum = 0;
for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
sum += nums[j];
if (sum >= s) { // 一旦发现子序列和超过了s,更新result
subLength = j - i + 1; // 取子序列的长度
result = result < subLength ? result : subLength;
break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
}
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
而对于这个代码进行分析的话,我们会发现他的时间复杂度是O(n2),实际上对于这个量级的数据,我们这种算法实际上已经需要更新的了,很容易超时。
那有没有一种方法可以有效的减少时间复杂度呢?回忆一下之前part2部分的内容,我们实际上是需要两层for循环来遍历数组的事情,我们采用双指针法来实现得到了一层循环做到了两层循环做到的事情,故双指针法是一条道路。
双指针法的实现过程:
int function1(int target, vector<int>& nums) {
//首先我们先可以看到这里,我们决定采用双指针法,来仅仅采用一次循环达到题目所需要的要求。
//先定义具体的指针。sum的值实际上就是数组从begin开始加到end为止
int begin = 0, end = 0;
int sum = 0;
//对于我们来说,我们第一步的想法肯定是,遍历begin然后调整end来进行控制中间数据的叠加,但是当你写下begin++开始变化的时候
//你会发现,后面的end随之变化,我们最后写出来的代码实际上也就是跟两层for循环一样,实际上也就是和暴力算法没有区别。
for ( begin = 0; begin < nums.size(); begin++)
{
//定义sum 并且变化end
//for 循环从begin相加到end
}
}
上面的function的方式,我们似乎没有逃开两层循环的限制。所以我们来分析一下,为什么开始begin不能等于0,实际上我们想要让整个函数的运行仅仅只有一层for循环,我们所需要做的事情就是,随着循环的变化,begin和end仅仅只有一个自由度的变化。所以我们需要采用end的变化,来进行操作,看不懂的可以看看代码,然后理解一下过程。
int function1(int target, vector<int>& nums) {
//首先我们先可以看到这里,我们决定采用双指针法,来仅仅采用一次循环达到题目所需要的要求。
//先定义具体的指针。sum的值实际上就是数组从begin开始加到end为止
//current_len统计的是从begin到end之间的数据有多少个
//result返回最后的最短窗口 初始最大的int数,方便操作
int begin = 0, end = 0;
int sum = 0;
int current_len = 0, result = INT32_MAX;
//采用end++的变化
for ( end = 0; end < nums.size(); end++)
{
sum += nums[end];
//这里if注意一下,我们先按照自己感性的想法来写
if (sum >= target)
{
//当中间的数字大了,我们实际上就可以统计窗口的大小,并且将begin进一位,然后再次统计了。
current_len = end - begin + 1;
//选择小的窗口大小
result = result < current_len ? result : current_len;
//准备下一次的循环,我们发现现在的数据已经大于target了,所以我们需要减掉begin的值,将左指针向右移动,然后后续右指针再进行移动窗口,得到最后的值
sum -= nums[begin++];
}
}
//最后返回数组的大小,也就是result,但是不要忘记存在一个数组全部加起来都没有大于target,此时result的值为INT32_MAX
return result == INT32_MAX ? 0 : result;
}
但是这样子又有什么问题呢?这也是初学者最容易犯错的地方。
给一个样例,如果nums[1,1,1,1,1,100] target = 100的话会发生什么
我们用上面的代码进行运行,会发现在那个if的地方会存在问题,最开始begin = 0,end开始循环达到5的时候 sum才大于100,然后进行一系列计算 此时result = 6 sum = 104 ,问题就在sum上,sum此时还是大于100的数据,但是我们是if,直接进行后面的(end++)了,而此时sum是有问题的,因为我们此时应当做的事情是继续移动左边的begin缩小直到sum小于100,这样子后面移动end,才是有效的,所以那个地方应该采用while。
int function1(int target, vector<int>& nums) {
//首先我们先可以看到这里,我们决定采用双指针法,来仅仅采用一次循环达到题目所需要的要求。
//先定义具体的指针。sum的值实际上就是数组从begin开始加到end为止
//current_len统计的是从begin到end之间的数据有多少个
//result返回最后的最短窗口 初始最大的int数,方便操作
int begin = 0, end = 0;
int sum = 0;
int current_len = 0, result = INT32_MAX;
//采用end++的变化
for ( end = 0; end < nums.size(); end++)
{
sum += nums[end];
//这里if注意一下,我们先按照自己感性的想法来写
while (sum >= target)
{
//当中间的数字大了,我们实际上就可以统计窗口的大小,并且将begin进一位,然后再次统计了。
current_len = end - begin + 1;
//选择小的窗口大小
result = result < current_len ? result : current_len;
//准备下一次的循环,我们发现现在的数据已经大于target了,所以我们需要减掉begin的值,将左指针向右移动,然后后续右指针再进行移动窗口,得到最后的值
sum -= nums[begin++];
}
}
//最后返回数组的大小,也就是result,但是不要忘记存在一个数组全部加起来都没有大于target,此时result的值为INT32_MAX
return result == INT32_MAX ? 0 : result;
}
但是你会发现,此时又存在一个情况,也就是当如果begin的移动,让begin == end了呢?此时会发生什么情况,自己分析一下,也有助于理解这个过程,不行可以debug一下,这边只进行结论介绍,此时没有什么问题,也是可以继续使用,没有错。但我们这么想,当二者相等的时候如果还依然大于的时候,其实我们已经可以直接进行return 1节省步骤了,所以在此处可以进行小小的“优化”?。
这里为什么打 ? 呢,先按下不表,实际上如果在循环中加上一个判断语句,进行的优化,实际上是负面优化。
leetcode 209 长度最小的子数组
链接
基础见上面的题目,进行书写:
AC-code
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
leetcode 904 水果成篮
链接
class Solution {
public:
int totalFruit(vector<int>& fruits) {
//采水果的滑动窗口,小暴力算法
int begin = 0, end = 0;
int current_len = 0, max_len = 0;
int tag = 0, flag = 0;//用于标记 先按下不表
//定义两个篮子
vector<int >basket(2, -1);
for (end = 0; end < fruits.size(); end++)
{
//第一个篮子没装
if (basket[0] == -1)
{
current_len = 1;
max_len = max_len > current_len ? max_len : current_len;
basket[0] = fruits[end];
continue;
}
//第二个篮子没装,且出现新的类型水果,显然是下面的这段代码块
//第二个篮子没装,而没有出现新的类型的水果,此时我们需要做什么?
//想一下,发现我们仅仅需要让current_len(水果种类长度大小加一)。
//但是又想了一下,其实最后面统计current_len其实只需要通过end和begin的相加减,即可。故不需要额外定义
//不然此处的代码应当是:
//eles if(basket[1] == -1 )
//{
// if(basket[0] == fruits[end])
// {内部代码}
// else{内部代码
// }
// }
else if(basket[1] == -1 && basket[0] != fruits[end])
{
current_len += 1;
max_len = max_len > current_len ? max_len : current_len;
basket[1] = fruits[end];
continue;
}
//当篮子都确定了,此时出现水果 有两种情况
//第一种:水果出现过 其实实际上就是需要current_len++,同理于上,但是这边为什么要写,分析过程见下
//第二种:水果没有出现过,停止,统计总长度,并且重新决定begin的开始,故采用while
else if(basket[0] != -1 && basket[1] != -1 && basket[0] != fruits[end] && basket[1] != fruits[end])
{
current_len = end - begin;//注意没有减一,此时的end指向的是下一个元素.
max_len = max_len > current_len ? max_len : current_len;
//修改begin 值得关注的是,我们需要去判断begin的位置
//先按照给出的例子来总结规律
//第一个 0 1 2 2 第一次是0 1 begin要变成1,去掉0
//第二个 1 2 3 2 2 第一次是1 2 begin要变成1,去掉1
//比较奇怪的选项有1 2 1 3 就是夹着的,所以我们此处做的选择需要同时把 1 和 2删除,所以需要做的事情是从前往后走,找到第一个1之后的数据,为什么是这样子的,希望读者自己弄明白,我也在最后在申明
//而为了不浪费for循环的时间,所以我们将tag写在了下方,实际上这个是后续加的,希望读者明白这个分析过程
basket[0] = fruits[begin == tag ? tag + 1 : tag];
begin = begin == tag ? tag + 1 : tag;
tag = begin;
//但是此时我们又发现存在了问题,此时对于第二个篮子我们该怎么定义,这里实际上也可以分类讨论,但证明到这里,我们确实发现存粹的算法已经不适合了,hh也可以继续
//这边取个懒 归空, end从新开始
basket[1] = -1;
end = begin;
current_len = 1;
flag = 0;
continue;
}
else
{
current_len = end - begin + 1;//注意,此时的end指向.
max_len = max_len > current_len ? max_len : current_len;
}
//下面的情况是第一种,也就是三个篮子,但出现同种类型的水果
//剩下的情况就是需要更新tag用于表示实时等于begin的元素的位置
if (!flag && fruits[end] == basket[0])
{
flag = 1;
tag = end;
}
}
return max_len;
}
};
但是即使是这样子都是无法通过的,原因就在于不够简化,实际上内部数据存储了一个这样子的检验数:
阿巴阿巴,所以我们需要一种新的想法来帮助我们进行实现上述的代码.其实不会告诉你,改起来很麻烦,笔者也改不动的.
引入STL思想: 实际上生活中很多东西如果采用STL来写的话,实际上都会简单很多,但是既然选择刷题,这些东西就需要自己去实现,但也并不意味着脱离STL,个人观点还是,如果对于一道题中,你是否调用STL对于算法直接产生了很大的影响,那建议还是自己进行完成,如果在调用的过程中,只是利用了其特性,或者对于时间复杂度没有太大的影响,那就可以采用STL.
实际上就是加上一个哈希map,利用哈希map的特性进行缩减变化.
class Solution {
public:
int totalFruit(vector<int>& fruits) {
//定义字典集,前一个int代表水果类型,后一个int代表数量
unordered_map<int, int >dic;
int begin = 0, end = 0;
int max_len = 0;
for ( end = 0; end < fruits.size(); end++)
{
//利用int的默认初始化值为0,以及map对于一个没有出现在容器当中的元素,进行[]选取,实际上完成了初始化的过程的两个细节.
++dic[fruits[end]];
//经典while再现 当出现了两个字典的索引,代表出现了两种以上的水果
while (dic.size() > 2)
{
//返回迭代器 后续通过迭代器进行修改内部的值
auto iter = dic.find(fruits[begin]);
--iter->second;
if (iter->second == 0)
{
dic.erase(iter);
}
begin++;
}
max_len = max(max_len, end - begin + 1);
}
return max_len;
}
};
上头,这题有点难啃,建议读者做到这道题,可以几天内多看几遍,理解一下思路.
总结(后话):
此题一开始关于是采用模拟,还是采用分类的想法选择上,决定了两种不同的算法思想,关于分类,就是文中的第一段代码,明显复杂了很多,也需要考虑很多东西,而关于模拟的话,实际上采用stl 才简便,不然也是极度复杂的.
leetcode 76 最小覆盖子串
链接