单调栈——巧用栈解决“找到最近一个比其大的元素”问题

单调栈的定义

首先,介绍一下单调栈。其实就是栈内存放的数据是有序的。也因此可以分为单调递增栈和单调递减栈(即从栈底到栈顶的单调性)。
什么含义呢?我们以单调递减栈举例:现在有一组数3, 1, 4, 5, 3, 4假设从左向右入栈:
第一次:栈内为3
第二次:栈内为1, 3(因为堆栈先入后出的特性,因此1跑到前面去了)
第三次:如果直接入栈,则会变为4 1 3此时并不是单调性,因此我们应该将1和3pop掉,就留4
第四次:同第三次如果直接入栈,就会变成5 4着不是单调递增,因此只留5
第五次:入栈后为3 5符合单调递增
第六次:直接入栈 4 3 5此时也并不是单调性,但是仅仅pop掉3便可以维持单调性,因此结果为4 5

单调栈的模板

理解了上面那个保持单调性的动作,就可以写出模板了:我们假设对一组nums的数组进行排序

      int n = nums.size();
      stack<int>sk;
      //
      for(int i=0; i<n; i++)
      {
       	//本例以严格单调递增为例
       	//while的作用就是为了保证栈内的元素单调性而存在的
          while(!sk.empty() && nums[i] >= sk.top()) sk.pop();
          sk.push(nums[i]);
      }

最近一个比其大的元素问题?

有题目也可以知道,该栈常用于解决下一个更大元素此类的问题:给一个数组,要求你返回一个等长的数组,而数组种对应的索引返回储存着该索引之后第一个比该索引的值更大的元素(若没有更大的元素,就为-1)。
这个问题我们可以抽象成排队小人的身高——第一个索引代表第一个小人,第一个索引里的值代表第一个小人的身高。如下图的例子,第一个小人先看到的是第三个小人。
在这里插入图片描述
按照正常的思路来找:
首先正向一个for循环,然后每个元素再嵌套一个正向的for循环来找到第一个比其大的元素。这样的复杂度是 O ( n 2 ) O(n^2) O(n2)
若使用单调栈的思想:
一个逆向的for循环里面嵌套一个单调递减栈即可。那么嵌套一个单调栈也就意味着包含一个while,但是这个while里面只有pop操作,而push在外面是一次for循环的时候进去的,因此pop也仅仅只会每个元素出现一次。因此复杂度仅仅是 O ( n ) O(n) O(n)
【为何是逆向呢】因为我们找的是某个数右边第一个比其大的数,因此我们倒序遍历,可以确保需要找的数已经被处理过
【正向可以吗】正向也可以,只不过要修改思想为存储下标,我们就需要我们就将当前单调栈中所有对应值小于 n u m s [ i ] nums[i] nums[i] 的下标弹出单调栈,这些值的下一个更大元素即为 n u m s [ i ] nums[i] nums[i](此处需要用到下标来记录),最后把 i i i入栈。不如逆向好理解。
【总结循环方向】

  • 正向其实是在pop的时候,才进行查找的。毕竟是找右边的元素,因此要先走几步才知道。
  • 逆向就是先从后面从小到大排好,查找该元素右边的时候,就先检查一遍自身放进去仍符合从小到大(完成检查其实就是完成pop操作),这样就能直接找到第一个比它大的(毕竟里面是从小到大排的嘛)。

单调栈的应用

1 循环数组的下一个更大元素

在这里插入图片描述
注意,本题本题是循环数组,因此我们一开始的倒序for循环根本顾及不到对于最后一个元素,所以我们应该把循环数组拉直:复制该序列的前 n − 1 n−1 n1个元素拼接在原序列的后面(但实现起来并不需要,因为我们不需要对原数组做出改变)。

class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        int n = nums.size();
        vector<int>ans(n,-1);
        stack<int>sk;
        for(int i=2*n-1; i>=0; i--)
        {
            while(!sk.empty() && nums[i%n] >= sk.top()) sk.pop();
            if(!sk.empty()) ans[i%n] = sk.top();
            sk.push(nums[i%n]);
        }
        return ans;
    }
};

2 股票价格跨度(连续区间的较小值范围,pair对元素)

在这里插入图片描述
因为是从今天开始往回倒推的,因此栈中一旦出现一个大的数,就会阻断之间的联系。因此本题可以存一个pair,将下标(代表着天数)和价格放入。只要能越过最近最大值的山,就能保证前后最大值都可以拿下。
【细节】注意,可以通过放一个哨兵(-1,INT_MAX)来减少判断

class StockSpanner {
public:
    stack<pair<int, int>> sk;
    int index;
    StockSpanner():index(0) {
        sk.push({-1, INT_MAX});
    }
    int next(int price) {
        while(price >= sk.top().second) sk.pop();
        int ans = index - sk.top().first;
        sk.push({index, price});
        ++index;
        return ans;
    }
};

3 每日温度(实际场景的应用)

在这里插入图片描述
其实这个题目就能看出是找一个最近更高温度的天,因此就符合题意,直接单调栈处理(就是处理下标)即可。

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        int n = temperatures.size();
        stack<int>sk;
        vector<int> ans(n);
        for(int i=n-1; i>=0; i--)
        {
            while(!sk.empty() && temperatures[i] >= temperatures[sk.top()]) sk.pop();
            if(!sk.empty()) ans[i] = sk.top()-i;
            else ans[i] = 0;
            sk.push(i);
        }
        return ans;
    }
};

4 132模式(充分理解单调栈的题)

该题的数据量只允许使用一次遍历!
在这里插入图片描述
这个题乍一看怎么会用到单调栈的呢?我们来慢慢分析:
我们以枚举 i i i开始:由132的结构可知,我们相当于要从 i i i 的后面找到两个数(以 j , k j,k j,k 为例)。两个数首先满足都大于 i i i 指向的数。其次 j < k j<k j<k,但 j j j 指向的数大于 k k k。由于我们的遍历是单向的,因此我们可以将问题转化为找 k k k,首先 k k k 需要比 i i i 大,同时在[i, k]之间存在比 k k k大的数即可。
【关键思想】那么关键问题就是,如何查询找 k k k和 比k还大的 j j j。而单调栈便可以解决该问题:首先,单调栈是解决找一个最近更大的元素,因此可以由i方便找到 j j j ,而逆序遍历构成的单调递增栈中的 j j j 后面都是比 j j j要大的元素,而比 j j j小的元素都pop掉了。因此我们可以从pop的地方下手,找到我们想要的 k k k

class Solution {
public:
    bool find132pattern(vector<int>& nums) {
        int n = nums.size();
        int k = INT_MIN;
        stack<int>sk;
        for(int i=n-1; i>=0; i--)
        {
            while(!sk.empty() && nums[i]>sk.top()) 
            {
                k = max(k,sk.top());
                sk.pop();
            }
            //当有k值存在时,便代表着堆栈中已经有数了,因此只判断k即可
            if(nums[i]<k) return true;
            sk.push(nums[i]);
        }
        return false;
    }
};

5 接雨水(面试常考)(充分理解单调栈的题)

在这里插入图片描述
其实就只看图,也能感受到,用单调栈来解决!那么栈内存放的就是这些墙,我们可以看到两个规律:

  • 当前高度小于栈顶墙高度时,代表这里会有积水(但是要避免是边界情况),此时我们应该将墙的下标入栈
  • 当前高度大于等于栈顶墙高度时,代表积水会在这里结束,我们此时应该计算之间有多少积水了,计算完再将该墙下标入栈,以供之后的计算。
    【程序分析】
    这样分析,那么就是使用一个单调递减栈,当前高度大于等于栈顶高度时,将会pop掉需要计算积水的墙体,此时进行计算。否则,当小于的时候,直接入栈即可。
    【计算过程分析】我们先看如何去计算?
    其实就是每抽出一个积水的墙体,都进行该墙体的栈内左边墙体和当前墙体的高度比较,然后是根据木桶原理,积水的高度跟高度矮的积水相同,并且积的水为高度差,即: 积水的高度 = m i n ( 凹槽左边高度,凹槽右边高度 ) − 凹槽底部高度 积水的高度 = min(凹槽左边高度,凹槽右边高度)-凹槽底部高度 积水的高度=min(凹槽左边高度,凹槽右边高度)凹槽底部高度,而积水的宽度就是
    积水的宽度 = 凹槽的右边下标 − 凹槽的左边下标 − 1 积水的宽度 = 凹槽的右边下标-凹槽的左边下标-1 积水的宽度=凹槽的右边下标凹槽的左边下标1。最终 积水的体积 = 积水的高度 − 积水的宽度 积水的体积=积水的高度-积水的宽度 积水的体积=积水的高度积水的宽度
    这是建立在凹槽有左边的情况,若没有,那就是一个单纯的梯形,那样是没有积水的,因此这种情况直接退出讨论即可。
class Solution {
public:
    int trap(vector<int>& height) {
        int n = height.size();
        stack<int>sk;
        int ans = 0;
        for(int i=0; i<n; i++)
        {
            while(!sk.empty() && height[i] >= height[sk.top()])
            {
                int cur = sk.top();
                sk.pop();
                if(sk.empty()) break;
                int r = i;
                int l = sk.top();
                int h = min(height[r],height[l]) - height[cur];
                ans += (r-l-1)*h;
            }
            sk.push(i); 
        }
        return ans;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值