数组--part 4--长度最小的子数组(力扣299/904/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 最小覆盖子串

链接

经过上一题的滑动窗口的过度,相信读者也知道此处改用上的是哈希表了,故不写出不含哈希表的写法。

class Solution {
public:
    bool compare(unordered_map<char, int>& svec, unordered_map<char, int>& tvec)
    {
        if (svec.size() == tvec.size())
        {
            for (auto i : tvec)
            {
                if (svec[i.first] < i.second)
                {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    string minWindow(string s, string t) {
        //首先常规,而且我们定义两个map进行承接计算。
        // result初始化为10000个x是为了后续比较大小取小的数,方便一下,不然下面需要写一个if
        // tag用于判断该result是否进行修改过。
        unordered_map<char, int>svec;
        unordered_map<char, int>tvec;
        int begin = 0, end = 0;
        string result(10000,'x');
        bool tag = false;
        int current_len = 0;
        //首先先统计字典,判断字串t当中含有的字符。
        for (auto i : t)
            ++tvec[i];
        //然后通过经典的滑动窗口,进行遍历s字符串。
        for (end = 0; end < s.size(); end++)
        {
            //此处用于更新
            if (tvec.find(s[end]) != tvec.end())
            {
                ++svec[s[end]];
            }
            //此处用于修改begin来收缩数组。此处分析一下有几种可能
            //情况1: A A B C
            //需要关注的是,在循环遍历的时候while的条件并不是只是需要size和vale符合停止循环,他还需要此时s[begin]指向的ABC中之一,读者可以进行分析一下为什么
            //如果分析不出来,可以将此句删除,然后调试看看。
            while (compare(svec,tvec) || tvec.find(s[begin]) == tvec.end())
            {
                //当svec的长度=3的时候更新begin 避免出现情况1的错误
                if (compare(svec,tvec))
                {
                    tag = true;
                    current_len = end - begin + 1;
                    result = result.size() < current_len ? result : s.substr(begin, current_len);
                }
                //begin不能超过end
                if (begin < end) 
                {
                    --svec[s[begin]];
                    if (svec[s[begin]] <= 0)
                    {
                        svec.erase(svec.find(s[begin]));
                    }
                    begin++;
                }
                else
                {
                    break;
                }
            }

        }
        return result = tag == true ? result : "";
    }   
};

运行结果:
在这里插入图片描述
数据一看,好家伙666,我以为10000个x已经是输入的极限了。。。太年轻了
在这里插入图片描述
感觉题解的方法和我的差不多,理解思想吧。 后续的修改就由读者自己完成吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值