教你如何使用单调栈解题

开篇

懒癌晚期最近疯狂发作,连着三四天没有更新博客,没有刷PAT,感觉到了最熟悉的期末搁置阶段(简称越到期末越放松)。今天终于提起精神来完成每日任务了,希望在离开家的前几天可以每天更新一篇面试类文章吧。
今天我们仍然说数据结构(算法部分暂时搁置,因为本人最近也在充实算法领域的其他新鲜知识,等把最近的那本书看完再来继续更新纯算法博客).今天是一种非常重要的数据结构的衍生物——单调栈,至于什么是单调栈呢,接着往下看吧。

单调栈

众所周知,栈是我们在解题的时候用的非常多一个数据结构,你可能不知道什么Bellman-Ford算法,也不知道SPFA算法,但是你一定直到深度优先遍历算法,简称DFS算法,其中我们使用的数据结构就是栈,而BFS使用的是队列。深度优先讲究的是一个一条路走到黑,而栈是后进先出,也就是我们我们连续地从栈中取出的元素,一定可以构成一条从起点到达终点的路径。
扯远了,今天我们主要说的栈的一种特殊结构——单调栈。顾名思义,单调栈就是一种单调的栈。至于这种单调可能是单调递增也可能是单调递减,但只要满足内部存储的元素是单调的就满足单调栈的特点。
听起来有点像堆?其实不是的,单调栈用途并不是特别广泛,我也是在以前面试的时候,包括在力扣上遇到了类似问题,所以今天才拿出来说一下,单调栈只处理一种典型的问题,叫做Next Greater Element。本文用讲解单调队列的算法模板解决这类问题,并且探讨处理循环数组的策略。
举个例子,给你一个数组[2,1,2,4,3],我们返回数组[4,2,4,-1,-1]
解释:第一个2后面第一个比2大的数是4;1后面第一个比1大的数是2;第二个2后面第一个比2大的数是4;4后面没有比4大的数,填-1;3后面没有比3大的数,填-1.
所以我们可以看出来,我们要找到的是某个数字后面第一个比它大的数,如果没有则填-1。这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了,但是暴力解法的时间复杂度为O(n^2)。
根据labuladong大佬的文章,这个问题可以抽象思考一下:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如果求元素2的Next Greater Number?很简单,如果能看到元素2,那么他后面可见的第一个人就2的Next Greater Number,因为比2小的元素身高不够,都被2挡住了,第一个露出来的就是答案。
在这里插入图片描述
我们可以看到,第一个2后面可以直接看到2的就是4,3也被4挡住了。1后面第一个能看到1的是2,以此类推。我们先来看下代码:

#include <vector>
#include <stack>
using namespace std;
vector<int> nextGreaterNumber(vector<int>& nums)
{
    vector<int> ans(nums.size());//存放答案的数组
    stack<int>s;//声明一个栈
    for(int i = nums.size()-1;i >=0 ;i--)
    {
        //倒着放入栈中
        while(!s.empty()&&s.top()<=nums[i])//判定个子高矮
            s.pop();//不要矮子,我们只找高的
        ans[i] = s.empty() ? -1 : s.top();//这个元素身后的第一个高个子
        s.push(nums[i]);//进队,需要与接下来的身高作比较了
    }
    return ans;
}

这就是单调队列解决问题的模板,我们必须从后向前遍历数组将其放入栈中,因为栈中是我们用来比较的元素,而我们找的是每个元素后面比其大的元素,所以这个模板很容易理解。
这个算法的时间复杂度不是那么直观,如果你看到for循环嵌套while循环就认为这个算法的时间复杂度是O(n^2)的话就大错特错了,这个算法的时间复杂度其实是O(n)。
我们从整体看:总共有n个元素,每个元素都被push入栈了一次,而最多会被pop一次,没有任何冗余操作。所有总的计算规模是和元素规模n成正比的,也就是O(n)的复杂度。
现在相信大家应该已经掌握了单调栈的基本模板以及明确了其时间复杂度的推导,下面让我们来看一个加深的版本。

每日温度问题

下面这个问题出自leetcode,相信好多朋友都遇到过了,下面我们把题目贴过来:
在这里插入图片描述
解释:第一天华氏度73,第二天74华氏度,比73大,所以对于第一天,只要等一天就能等到一个更暖和的气温。后面的同理。你已经对Next Greater Number类型问题有些敏感了,这个问题本质上也是找Next Greater Number,只不过现在不是问你Next Greater Number是多少,而是问你当前距离Next Greater Number的距离而已。我们直接上代码:

   vector<int> dailyTemperatures(vector<int>& temperatures) {
       int n = temperatures.size();
       //先设置res全部为0,因为最后一位一定是6,如果栈内有n个元素,就说明后n个是0 
       vector<int> res(n, 0);
       stack<int> st;
       for (int i = 0; i < temperatures.size(); ++i) {
           while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
               auto t = st.top(); st.pop();
               res[t] = i - t;
           }
           st.push(i);
       }
       return res;
   }

单调栈与循环数组

同样是Next Greater Number,现在假设给你的数组是一个环形的,如何处理呢?
给你一个数组[2,1,2,4,3],返回数组数组[4,2,4,-1,4]。拥有了环形属性,最后一个元素3绕了一圈后找到了比它自己大的元素4.
在这里插入图片描述
但是计算机是没有环形内存的,所以我们应该如何来表示一个环形数组呢?一般是通过%运算符求模,获得环形特效:

int arr[5] = {1,2,3,4,5};
int n = arr.size();
int index = 0;
while(true)
{
	cout<<arr[index%n];
	index++;
}

回到原问题,现在由于加入了环形属性,所以现在要找的比当前元素大的元素不一定出现在当前元素的右边了,也有可能出现在左边。我们可以考虑这样的思路:将原始数组翻倍,就是在后面再接一个原始数组,这样的话我们可以在解题的时候仍然只取右边的元素,但是这样一来某个元素左边的元素也会出现在其右边,这样的话我们就可以做到左右兼顾了,完成了环形属性
在这里插入图片描述
怎么实现呢?其实就是把双倍数组构造出来然后套用我们上面的算法模板。但是我们不可以构造新数组,而是利用循环数组的技巧来模拟。直接上代码:

#include <vector>
#include <stack>
using namespace std;
vector<int> nextGreaterNumber(vector<int>& nums)
{
    int n = nums.size();
    vector<int> res(n);//存放结果
    stack<int>s;
    //假装这个数组的长度翻倍了
    for(int i = 2 * n - 1;i >= 0;i--)
    {
        while(!s.empty() && nums[i % n] >= s.top())
            s.pop();
        res[i%n] = s.empty() ? -1 : s.top();
        s.push(nums[i%n]);
    }
    return res;
}

总结

至此关于单调栈的问题已经全部说完了,这三种问题应该可以解决所有的有关单调栈的问题了。而其中我们又学到了一个新思路,对于需要左右兼顾的数组,我们应该将数组扩充为原始长度的2倍,然后取模定位索引。OK,希望每一篇博客都会对大家有帮助,自己也会在离开家前的这几天每天坚持更新,入职以后怕是面试类文章要更的很慢啦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值