代码随想录算法训练营第60期第五十一天打卡
大家好,昨天我们结束了动态规划的题目,其实我们可能还没有完全理解那些题目的真正含义,那其实很正常大家多复习几遍就可以了,那我们今天就将开始一个全新的章节,它就是单调栈,那关于什么是单调栈,我们如何使用单调栈解决题目,我们讲解之后大家就会一目了然,我们就开始今天的题目。
第一题对应力扣编号为739的题目每日温度
其实我们在开始这道题目的时候我们需要先讲解一下单调栈的理论基础,不然大家压根不知道什么是单调栈,通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n)。单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。当然我们在使用单调栈的时候,我们需要注意以下几点:1.单调栈里存放的元素是什么?单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接T[i]就可以获取。第二点单调栈里元素是递增呢? 还是递减呢?这个其实顺序的描述为 从栈头到栈底的顺序,这里我们要使用递增循序(再强调一下是指从栈头到栈底的顺序),因为只有递增的时候,栈里要加入一个元素i的时候,才知道栈顶元素在数组中右面第一个比栈顶元素大的元素是i。其实我们就直接看一下这道题目的题目要求:
题目是要求我们求下一个比自身大的元素的位置,其实很明显我们通过上面对单调栈的了解我们就可以推断这道题目我们就可以使用单调栈来解决,那我们就一起来看一下题目的解题思路:其实这道题目我要把每一个地方都给大家模拟一年比较麻烦,我就给大家解释一部分剩下的都是同理的,我们首先要搞清楚我们这里单调栈我们是使用单调递增的单调栈,我们使用单调栈存在如下几种情况:(1)当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况(2)当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况(3)当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况,那我们就看一下本题的大致的模拟单调栈的过程,首先先将第一个遍历元素73加入单调栈,随后加入T[1] = 74,因为T[1] > T[0](当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况)。如果我们要保持一个递增单调栈(从栈头到栈底),所以将T[0]弹出,T[1]加入,此时result数组可以记录了,result[0] = 1,即T[0]右面第一个比T[0]大的元素是T[1]。其实后面的都是这种思路,我们继续看几个,下一个元素是75,同理,T[1]弹出,这样我们就可以更新result数组,我们result数组起初都是初始化为0,这样其实我们找到最大值的时候由于右边不存在比最大值还大的元素就不会更新保持为0,接下来就是加入T[3] = 71,T[3] < T[2] (当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况),加T[3]加入单调栈。加入T[4],T[4] == T[3] (当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况),此时依然要加入栈,不用计算距离,因为我们要求的是右面第一个大于本元素的位置,而不是大于等于!到这里大家差不多就可以明白我们单调栈的模拟过程,我们是跟栈顶元素比较,这样的话我们就尝试给出本题的解题代码:
class Solution {
public:vector<int> dailyTemperatures(vector<int>& temperatures) {stack<int> st; //定义单调栈//定义我们的结果数组里面存储的就是下一个更高温出现在几天后vector<int> result(temperatures.size(), 0);//注意我们存入单调栈的元素应该是数组下标不是元素的具体值st.push(0);for (int i = 1; i < temperatures.size(); ++i){//如果当前的值小于栈顶元素其实它并不会影响我们寻找比当前栈顶元素的温度高的下一个高温出现在几天后if (temperatures[i] < temperatures[st.top()]){st.push(i);}else if (temperatures[i] == temperatures[st.top()]){st.push(i);}else{while(!st.empty() && temperatures[i] > temperatures[st.top()]){//此时我们其实就可以找到当前栈顶元素的温度高的下一个高温出现在几天后result[st.top()] = i - st.top();//找完了的要pop掉让下面的元素继续找st.pop();}st.push(i);}}return result;}
};
其实大家要注意我们要去理解单调栈的模拟过程,我们其实只有发现比当前栈顶元素大的元素的时候我们才可以得到比当前栈顶元素温度高的下一个天,还有大家注意我们的单调栈里面存放的应该是元素的下标而不是元素值,这样我们方便求是第几天,大家注意这一些就可以。
第二题对应力扣编号为496的题目下一个更大元素 I
我们来到今天的第二题,经过上一道题大家应该可以对单调栈有大致的了解,那其实听到这道题我们就可以看出本题就是使用单调栈来解决,因为题目要求我们求的是比当前元素大的下一个元素,我们就先来看一下题目的具体要求:
题目意思稍微有点抽象,其实我们是返回一个nums1长度的数组,我们是需要先在nums2中找到与nums1各个元素相等的对应位置的下标,然后在这个下标之后去找第一个大于nums1中元素的元素值,注意我们是要找到具体的元素不是下标,这样我们很清楚这道题我们一定是使用单调栈来解决,首先注意题目说如果不存在对应位置就输出 -1 ,所以result数组如果某位置没有被赋值,那么就应该是是-1,所以就初始化为-1。在遍历nums2的过程中,我们要判断nums2[i]是否在nums1中出现过,因为最后是要根据nums1元素的下标来更新result数组。注意题目中说是两个没有重复元素 的数组 nums1 和 nums2。就是这两个数组中每两个元素都是不同的,没有重复元素,我们就可以用map来做映射了。根据数值快速找到下标,还可以判断nums2[i]是否在nums1中出现过。这个其实在哈希表章节里面提到过,
本题单调栈栈头到栈底的顺序,要从小到大,也就是保持栈里的元素为递增顺序。只要保持递增,才能找到右边第一个比自己大的元素。这样的话我们就还是考虑几种情况:
- 情况一:当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况
- 情况二:当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
- 情况三:当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况
第一种情况:此时满足递增栈(栈头到栈底的顺序),所以直接入栈。第二种情况:如果相等的话,依然直接入栈,因为我们要求的是右边第一个比自己大的元素,而不是大于等于!第三种情况:此时如果入栈就不满足递增栈了,这也是找到右边第一个比自己大的元素的时候。判断栈顶元素是否在nums1里出现过,(注意栈里的元素是nums2的元素),如果出现过,开始记录结果。因为我们题目所求的元素都是来自nums2,此时栈顶元素在nums2数组中右面第一个大的元素是nums2[i],这里其实很绕容易迷糊。这样的话我们经过以上的分析就可以尝试给出代码:
class Solution {
public:vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {//定义单调栈stack<int> st;//定义存放结果的数组vector<int> result(nums1.size(), -1);//特殊情况最好考虑进来if (nums1.size() == 0) return result;unordered_map<int, int> umap;for (int i = 0; i < nums1.size(); ++i) umap[nums1[i]] = i;//模拟单调栈的过程st.push(0);for (int i = 1; i < nums2.size(); ++i){if (nums2[i] < nums2[st.top()]) st.push(i);else if (nums2[i] == nums2[st.top()]) st.push(i);else{while(!st.empty() && nums2[i] > nums2[st.top()]){//首先需要去看当前的元素是否在nums1中出现过if (umap.count(nums2[st.top()]) > 0){//如果出现过我们就可以收集结果了注意这个地方对应的是nums1的索引其实不难理解因为题目就是返回nums1长度的数组//而且我们的操作对象其实也是nums1里面的元素int index = umap[nums2[st.top()]];result[index] = nums2[i];}st.pop();}st.push(i);}}return result;}
};
这道题目的难度比较大,不仅仅涉及到了我们目前的单调栈的相关理论知识还涉及到了我们以前的哈希表的相关理论知识,包括我们如何判断一个数组里的元素是否在其他数组出现过,这个需要大家去复习而且需要综合使用我们以前学过的所有方法,难度较大,大家慢慢理解。
第三题对应力扣编号503的题目下一个更大元素II
这是我们今天的最后一道题目,其实与上一道题目一定是异曲同工的,只不过上一道题目其实一点也不简单,这道题目估计也不会简单,那我们就直接看一下这道题目的题目要求:
这道题其实有点奇怪的地方就是我们可以循环搜索这个数组,大家看我们上面的示例,其实我们的数组表面上是[1,2,1]其实是[1,2,1,1,2,1,1,2,1....]这样我们自然可以找到比最后一个1大的元素就是2,那我们就看看这道题的解题思路:其实这道题大家时候考虑我将两个数组接起来然后使用我们的单调栈来解决问题,这个其实是可以的,如果存在这样的元素的话其实我们把数组重复一遍就可以,那我们可以先尝试一下这种解题思路:
class Solution {
public:vector<int> nextGreaterElements(vector<int>& nums) {//数组重复的过程vector<int> nums1(nums.begin(), nums.end());nums.insert(nums.end(), nums1.begin(), nums1.end());//初始化result数组vector<int> result(nums.size(), -1);if (nums.size() == 0) return result;//开始定义单调栈stack<int> st;st.push(0);for (int i = 1; i < nums.size(); ++i){if (nums[i] < nums[st.top()]) st.push(i);else if (nums[i] == nums[st.top()]) st.push(i);else{while(!st.empty() && nums[i] > nums[st.top()]){result[st.top()] = nums[i];st.pop();}st.push(i);}}result.resize(nums.size() / 2);return result;}
};
大家如果明白了上一道题目相信这种思路并不难理解,其实大家一刷把这道题理解道这种程度已经很不错了,但这样其实很耗费空间,而且做了很多的无用操作,其实为了避免一些无效操作我们可以直接单调栈模拟的过程就跑两遍数组:
class Solution {
public:vector<int> nextGreaterElements(vector<int>& nums) {vector<int> result(nums.size(), -1);if (nums.size() == 0) return result;stack<int> st;st.push(0);for (int i = 1; i < nums.size() * 2; i++) { // 模拟遍历两边nums,注意一下都是用i % nums.size()来操作if (nums[i % nums.size()] < nums[st.top()]) st.push(i % nums.size());else if (nums[i % nums.size()] == nums[st.top()]) st.push(i % nums.size()); else {while (!st.empty() && nums[i % nums.size()] > nums[st.top()]) {result[st.top()] = nums[i % nums.size()];st.pop();}st.push(i % nums.size());}}return result;}
};
今日总结
今天的单调栈其实是一种很巧妙的做法,希望大家理解这种做法,还有我们要清楚我们单调栈是如何使用的就可以了,今天我们的分享就到这里,我们明天再见!