单调栈题解

1    九阴真经第一式 :单调栈
1.1    代表题目:84. 柱状图中最大的矩形
单调栈是一种理解起来很容易,但是运用起来并不那么简单的数据结构。一句话解释单调栈,就是一个栈,里面的元素的大小按照他们所在栈内的位置,满足一定的单调性。那么到底什么时候用这个单调栈,怎么用单调栈呢。下面我们来看几个例子。
题目是这样的,给一个数组,返回一个大小相同的数组。返回的数组的第i个位置的值应当是,对于原数组中的第i个元素,至少往右走多少步,才能遇到一个比自己大的元素(如果之后没有比自己大的元素,或者已经是最后一个元素,则在返回数组的对应位置放上-1)。
简单的例子:
input: 5,  3,  1,  2,  4
return: -1  3  1  1  -1
explaination: 对于第0个数字5,之后没有比它更大的数字,因此是-1,对于第1个数字3,需要走3步才能达到4(第一个比3大的元素),对于第2和第3个数字,都只需要走1步,就可以遇到比自己大的元素。对于最后一个数字4,因为之后没有更多的元素,所以是-1。
暴力做的结果就是O(n^2)的时间复杂度,例如对于一个单调递减的数组,每次都要走到数组的末尾。那么用单调栈怎么做呢?先来看代码:
vector<int> nextExceed(vector<int> &input) {
    vector<int> result (input.size(), -1);
    stack<int> monoStack;
    for(int i = 0; i < input.size(); ++i) {    
        while(!monoStack.empty() && input[monoStack.top()] < input[i]) {
            result[monoStack.top()] = i - monoStack.top();
            monoStack.pop();
        }
        monoStack.push(i);
    }
    return result;
}
我们维护这样一个单调递减的stack,stack内部存的是原数组的每个index。每当我们遇到一个比当前栈顶所对应的数(就是input[monoStack.top()])大的数的时候,我们就遇到了一个“大数“。这个”大数“比它之前多少个数大我们不知道,但是至少比当前栈顶所对应的数大。我们弹出栈内所有对应数比这个数小的栈内元素,并更新它们在返回数组中对应位置的值。因为这个栈本身的单调性,当我们栈顶元素所对应的数比这个元素大的时候,我们可以保证,栈内所有元素都比这个元素大。对于每一个元素,当它出栈的时候,说明它遇到了自己的next greater element,我们也就要更新return数组中的对应位置的值。如果一个元素一直不曾出栈,那么说明不存在next greater element,我们也就不用更新return数组了。
这里作者在数组末尾加入了一个height 0,来强迫程序在结束前,将所有元素按照顺序弹出栈。是一个很巧妙的想法。在这个例子中,对于每一个元素都只有一次入栈和出栈的操作,因此时间复杂度只有O(n)。

Leetcode 84. Largest Rectangle in Histogram

我们需要分析一下,每一个元素都要入栈一次,出栈一次。入栈的时候是for loop的iteration走到它的时候,那出栈的时候意味着什么呢。想清楚了这一点,我们也就理解了上面的答案。在上一题,每个元素出栈,是说明它找到了它在原数组中的next greater element.那这道题呢?元素出栈,意味着,我们已经计算了以它的顶为上边框的最大矩形。首先我们可以通过反证法轻松证明,最后的结果中的最大矩形的上边框,一定和某一个bar的顶重合,否则我们一定可以通过提高上边框来增加这个矩形的面积。这一步之后,我们还需要理解,这时候我们计算的矩形的左右边框都已经到达了极限。结合栈内元素的单调性,我们知道左边的边框是栈顶的元素+1,栈顶元素所对应的bar一定比出栈元素对应的bar小,所以以出栈元素对应的bar为高的矩形无法往左边延展。结合代码,我们知道右边的边框是正在处理的i,因为我们已经判断过这个第i个元素所对应的bar也一定比出栈元素对应的bar小,所以矩形无法往右边延展。这个元素和左右边框之间如果还有空隙,那么这些空隙里所存在的bar,一定是因为维护栈的单调性而被弹出了。换言之,这些bar如果存在,那么一定比这个出栈元素所对应的bar高。既然这些bar的高度更高,那么就可以被纳入这个最大矩形的计算中(例如一个“凹”字)。因此我们证明了,当我们将第i个元素弹出栈的时候,我们计算了以hight[i]为高的最大矩形的面积。

Leetcode 85. Maximal Rectangle

单调栈这种数据结构,通常应用在一维数组上。如果遇到的问题,和前后元素之间的大小关系有关系的话,(例如第一题中我们要找比某个元素大的元素,第二个题目中,前后的bar的高低影响了最终矩形的计算),我们可以试图用单调栈来解决。在思考如何使用单调栈的时候,可以回忆一下这两题的解题套路,然后想清楚,如果使用单调栈,每个元素出栈时候的意义。最后的时间复杂度,因为每个元素都出栈入栈各一次,所以是线性时间的复杂度。

1.2.1    单调栈的性质:

1.单调栈里的元素具有单调性;
2.元素加入栈前,会在栈顶端把破坏栈单调性的元素都删除;
3.使用单调栈可以找到元素向左遍历第一个比他小的元素,也可以找到元素向左遍历第一个比他大的元素。
对于第三条性质的解释(最常用的性质):
对于单调栈的第三条性质,你可能会产生疑问,为什么使用单调栈可以找到元素向左遍历第一个比他大的元素,而不是最后一个比他大的元素呢?我们可以从单调栈中元素的单调性来解释这个问题,由于单调栈中的元素只能是单调递增或者是单调递减的,所以我们可以分别讨论这两种情况(假设不存在两个相同的元素):
1.当单调栈中的元素是单调递增的时候,根据上面我们从数组的角度阐述单调栈的性质的叙述,可以得出:
(1).当a > b 时,则将元素a插入栈顶,新的栈顶则为a
(2).当a < b 时,则将从当前栈顶位置向前查找(边查找,栈顶元素边出栈),直到找到第一个比a小的数,停止查找,将元素a插入栈顶(在当前找到的数之后,即此时元素a找到了自己的“位置”)
2.当单调栈中的元素是单调递减的时候,则有:
(1).当a < b 时,则将元素a插入栈顶,新的栈顶则为a
(2).当a > b 时,则将从当前栈顶位置向前查找(边查找,栈顶元素边出栈),直到找到第一个比a大的数,停止查找,将元素a
插入栈顶(在当前找到的数之后,即此时元素a找到了自己的“位置”)


 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页