单调栈

总得来说这相当于是一堂“习题课”。
Theme is “Monum Stack”.
(记得上次仿佛说还要讲一哈优先队列priority_queue,但着实又感觉没有啥特别好讲的,其类似于一个大顶堆(heap,我们会在排序章节讲到堆排序)。就是说基本情况下,优先队列的总是会把最大的数放在队头!!)
本次就由浅入深准备了三道题,现在来集合成一个题解。

题一:

单调栈_第1张图片
相信见到这种题目甭管大神不大神第一思路都是暴力吧, 因为简单,通俗易懂。
暴力思路如下:
每遍历到一个数就从其之后开始寻找第一个比其大的数,找到的话就用下标差记入答案中,否则记入0到答案中。
代码就不给出了,确实是非常简单的了。但简单的代码存在了一个巨大的问题就是其是O(n²)的时间复杂度,这意味着你面试可能就gg了。

那既然我们开篇就给出了单调栈的思想,我们如何来设计并且维护呢?
思路如下:
每每遍历到一个数字我们就将其与栈顶元素去进行比较。
如果栈为空(即无栈顶元素),直接将此下标入栈。
如果此元素比栈顶元素小,那么也直接入栈。
倘若比当前栈顶元素要大!那么我们就相当于找到了第一个比当前栈顶元素的气温要高的日子,用下标相减得到等待天数。此外,我们将当前栈顶元素弹出,并且持续判断与栈顶元素的大小关系。如果仍旧比其大就重复上述操作。如果栈空了或者不比栈顶大了的话就将当前这个下标入栈。

通过以上操作我们可以发现比较高的气温会被压在栈底,而一旦找到了气温升高的日子就会从栈顶不断弹出元素并且持续进行比较。

我相信语言还是没法讲清楚,所以画个图解释或许会较好。
我们按照思路走一遍流程图:
入栈的都是下标!
单调栈_第2张图片
一开始栈为空,无需多余操作,直接入栈。
单调栈_第3张图片
下标为1时,由于74 > 73,直接会pop栈顶元素,然后会把答案,即间隔 1 - 0 = 1存入vector容器中。同时会把1入栈。
单调栈_第4张图片
此情况与上相同!不赘述!
单调栈_第5张图片
下标为5的气温会比4和3都要高,直接计算出答案弹出栈顶最后放入5即可。
后面的过程都是类似的,要注意的就是最后留在栈内的元素都是找不到比其更高的气温的,因此都是用0填充。

这里要注意一个小问题,答案容器要求返回vector,但实际上我们并不是按顺序一个一个计算出答案然后用push_back的方式存入答案的!

即注意上图,我们发现下标为4的会比下标为3的先计算出答案!!!
这样一来就要求我们要事先声明好容器的大小,然后直接利用索引的方式进行值的修改即可!
还有一点需要注意的就是直接声明大小会使此容器填充满0。
最后实现代码如下:

class Solution {
     
public:
    vector<int> dailyTemperatures(vector<int>& T) {
     
        vector<int> v(T.size()); // 直接声明了大小,会填充满0
        stack<int> s;
        for(int i = 0 ; i < T.size() ; i ++){
     
        // 不断与栈顶元素作比较,比其大的话就循环操作
            while(!s.empty() && T[i] > T[s.top()]){
     
            // 用下标索引的方式修改原先的0为计算出来的值
            // 不能直接push_back的原因上面有阐述
                v[s.top()] = i - s.top();
                s.pop();
            }
            s.push(i);
        }
        // 若栈内存在未处理的元素不需另外操作,因为我们初始化就是用0填充的
        // 因此现在无需修改。
        return v;
    }
};

单调栈_第6张图片

题二

此题是上次栈没有讲清楚的题最大矩形面积。
单调栈_第7张图片
肯定不排除那些看一眼代码就知道解题思路的大佬的存在啦。
不过呢,做事讲究个完整,还是把上次没讲的思路补上。
实际上这题的思想和上题类似,但需要多考虑一步!

算法思想如下:
遍历每一个元素。
如若栈为空,下标直接进栈。
如若栈不为空,如果遍历到的当前下标所对应的高小于栈顶元素对应的高那么由栈顶高所成的最大矩形就可以确定了。此矩形高就是栈顶元素,宽就是当前遍历下标和栈顶下标之差。然后栈顶元素出栈,遍历到的当前下标进栈。
如若栈不为空,如果当前遍历到的下标所对应的高大于栈顶元素对应的高那么栈顶元素所形成的矩形实际上还可以继续扩张面积仍旧不能确定。因此仅需让遍历到的当前下标进栈即可。

这儿之后会比上面的气温题多出一些步骤!
我们的栈也会存在遍历完一遍数组后栈中还有元素的现象。此时不能直接忽略,因为此时栈中残留元素的含义是:
以此下标对应的高,一直遍历到数组末尾都没有再比此高度更矮的数存在。其实翻译过来就是说,矩形一直可以从此下标开始延伸到数组末尾。
因此,在遍历完一遍数组后如果栈中有残余,就计算输入数组的size - 当前栈顶的下标 - 1为宽,且当前栈顶对应到数组索引值为高计算出面积。并且不断弹出栈顶元素直至处理完毕。

实际上思路真的和上题差不多,因此不再附图示,主要是后面多出来的这一步要好好理解。代码如下:

class Solution {
     
public:
    int largestRectangleArea(vector<int>& heights) {
     
        stack<int> s;
        int area = 0;
        for(int i = 0 ; i < heights.size() ; i ++){
     
            while(!s.empty() && heights[i] < heights[s.top()]){
     
                int height = heights[s.top()];
                s.pop();
                int width = i;
                if(!s.empty()){
     
                    width = i - s.top() - 1;
                }
                area = max(area, height * width);
            }
            s.push(i);
        }
        while(!s.empty()){
     
            int height = heights[s.top()];
            s.pop();
            int width = heights.size();
            if(!s.empty()){
     
                width = heights.size() - s.top() - 1;
            }
            area = max(area, height * width);
        }
        return area;
    }
};

此代码实际上还可以进行常数的优化,因此单纯上面的代码实际上效率还称不上特别高,如果想要深究可以看到官方题解----官方最大矩形题解。

题三

单调栈_第8张图片
**个人认为这道题是相较于前两题偏难一些的,纯属个人感觉哈。 **

首先我们要意识到雨水被接收在凹槽中,也就是说当存在一个两边高中间低的情况时我们就能求出雨水量。相信这个无论从常识还是理论方面都好理解。

于是乎,我们会维护一个递减单调栈!递增没意义啊,因为我们要的是两边高,中间低啊!

整体思路如下,以题目给出的例子为基准:
首先对于数组首元素,当其为0时实际上是无意义的,因此我们直接对索引为0的地方进行单独性判断。若值为0,就不予理睬,否则将下标进栈。

然后依次往后遍历元素,当遍历到的元素比栈顶元素严格小或者当前栈为空时就另其直接进栈。
单调栈_第9张图片
于是红框内两下标元素直接进栈!

再往后遍历就会遇到比栈顶要大的下标3,其对应高度为2。
由于单调栈的特性,我们知道,假如我们把当前栈顶元素弹出后栈还不为空的话那么必然就存在一个凹槽了。因为当前栈顶前肯定有比当前栈顶要高的元素的存在,又因为当前又遍历寻找到比当前栈顶高的元素,那么就证明可以积攒雨水了。

于是,当遍历到下标3时。我们会先弹出栈顶元素,然后判断栈是否为空。若为空,那么直接将现在遍历的下标3进栈。
如若不为空,我们取min(height[3], height[s.top()]),用此值来减去刚刚弹出的元素索引对应的高,乘上3 - s.top() - 1(代表矩形的宽),求出相应的雨水量。然后再次弹出栈顶,再做判断。

总得来说,就是当遍历到的当前高要严格大于当前栈顶索引对应的高时,有如下操作:
保存当前栈顶并弹出当前栈顶。
若栈空了,那么就是说明没有凹槽无法积攒雨水,直接跳出循环。
若此时栈未空!那么实际上当前遍历的高和当前栈顶对应的高就是高的两端,而刚刚弹出的元素就是那部分低的槽的高度。
画个抽象的容器。
单调栈_第10张图片
计算方法当然就是用两端里较矮的那个高来减去凹槽的高度最后乘上两个索引的差值作为宽得到盛水量。然后重复上述操作。

这个算法的思想实际上就是一层层地计算雨水量。

这里提供更为优秀清晰的题解—接雨水题解。

代码如下:

class Solution {
     
public:
    int trap(vector<int>& height) {
     
        if(height.size() == 0)
            return 0;
        stack<int> s;
        int water = 0;
        if(height[0] != 0)
            s.push(0);
        for(int i = 1 ; i < height.size() ; i ++){
     
            if(s.empty() || (!s.empty() && height[i] < height[s.top()]))
                s.push(i);
            else{
     
                while(!s.empty() && height[i] > height[s.top()]){
     
                    int temp = s.top();
                    s.pop();
                    if(!s.empty()){
     
                        water += (min(height[s.top()], height[i]) - height[temp]) * (i - s.top() - 1);
                    }
                    else{
     
                        break;
                    }
                }
                s.push(i);
            }
        }
        return water;
    }
};

单调栈_第11张图片
艰辛的错误历程就不赘述了。

快过年了,很放松,效率低下,damn!
单调栈_第12张图片
祝大噶活新年快乐啊!

你可能感兴趣的:(单调栈)