单调栈是一种理解起来很容易,但是运用起来并不那么简单的数据结构。一句话解释单调栈,就是一个栈,里面的元素的大小按照他们所在栈内的位置,满足一定的单调性。那么到底什么时候用这个单调栈,怎么用单调栈呢。下面我们来看几个例子。
先来分享一道非常简单的,我本人在google interview中遇到的题目。(大雾,当时并没有做出来。)
题目是这样的,给一个数组,返回一个大小相同的数组。返回的数组的第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)的时间复杂度,例如对于一个单调递减的数组,每次都要走到数组的末尾。那么用单调栈怎么做呢?先来看代码:
public int[] nexExceed(int[] input, int n){
int[] res = new int[n];
for (int i = 0; i < n; i++) {
res[i] = -1;
}
Stack stack = new Stack();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && (input[stack.peek()] < input[i])){
res[stack.peek()] = i-stack.peek();
stack.pop();
}
stack.push(i);
}
return res;
}
我们维护这样一个单调递减的stack,stack内部存的是原数组的每个index。每当我们遇到一个比当前栈顶所对应的数(就是input[monoStack.top()])大的数的时候,我们就遇到了一个“大数“。这个”大数“比它之前多少个数大我们不知道,但是至少比当前栈顶所对应的数大。我们弹出栈内所有对应数比这个数小的栈内元素,并更新它们在返回数组中对应位置的值。因为这个栈本身的单调性,当我们栈顶元素所对应的数比这个元素大的时候,我们可以保证,栈内所有元素都比这个元素大。对于每一个元素,当它出栈的时候,说明它遇到了自己的next greater element,我们也就要更新return数组中的对应位置的值。如果一个元素一直不曾出栈,那么说明不存在next greater element,我们也就不用更新return数组了。
这里作者在数组末尾加入了一个height 0,来强迫程序在结束前,将所有元素按照顺序弹出栈。是一个很巧妙的想法。在这个例子中,对于每一个元素都只有一次入栈和出栈的操作,因此时间复杂度只有O(n)。
解决了这个开胃菜,我们来看一道稍微复杂一点的题目。Leetcode 84. Largest Rectangle in Histogram
Given n non-negative integers representing the histogram's bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.
Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3].
The largest rectangle is shown in the shaded area, which has area = 10 unit.
For example,
Given heights = [2,1,5,6,2,3],
return 10.
int largestRectangleArea(vector<int> &height) {
int ret = 0;
height.push_back(0);
vector<int> index;
for(int i = 0; i < height.size(); i++) {
while(index.size() > 0 && height[index.back()] >= height[i]) {
int h = height[index.back()];
index.pop_back();
int sidx = index.size() > 0 ? index.back() : -1;
ret = max(ret, h * (i-sidx-1));
}
index.push_back(i);
}
return ret;
}
JAVA代码:
public int largestRectangleArea(int[] height) {
int len = height.length;
Stack s = new Stack();
int maxArea = 0;
for(int i = 0; i <= len; i++){
int h = (i == len ? 0 : height[i]);
if(s.isEmpty() || h >= height[s.peek()]){
s.push(i);
}else{
int tp = s.pop();
maxArea = Math.max(maxArea, height[tp] * (s.isEmpty() ? i : i - 1 - s.peek()));
i--;
}
}
return maxArea;
}
看上去这个解法长得和刚才那道题的差不多。实际算法也很相近,作者维护了一个单调递增的栈,然后进行了一系列xjb操作。那么到底为什么可以这么做?我们需要分析一下,每一个元素都要入栈一次,出栈一次。入栈的时候是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.(参考左神的算法书P26)
Given a 2D binary matrix filled with 0's and 1's, find the largest rectangle containing only 1's and return its area.
For example, given the following matrix:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
Return 6.
这道题可以轻松地转化成上一题。对于每一行,我们都可以构建一个histogram,然后计算。在构建新的histogram的时候,我们不需要全部遍历,只需要对已有的histogram进行略微的修改(运用DP的思想)。为了在视觉上更加清晰,我直接调用了上一题中的函数。思路还是异常简单的。
int maximalRectangle(vector<vector<char>>& matrix) {
if (matrix.empty()) return 0;
vector<int> height(matrix[0].size(), 0);
int maxRect= 0;
for(int i = 0; i < matrix.size(); ++i) {
for(int j = 0; j < height.size(); ++j) {
if(matrix[i][j] == '0') height[j] = 0;
else ++height[j];
}
maxRect = max(maxRect, largestRectangleArea(height));
height.pop_back();
}
return maxRect;
}
int largestRectangleArea(vector<int> &height) {
int ret = 0;
height.push_back(0);
vector<int> index;
for(int i = 0; i < height.size(); i++) {
while(index.size() > 0 && height[index.back()] >= height[i]) {
int h = height[index.back()];
index.pop_back();
int sidx = index.size() > 0 ? index.back() : -1;
ret = max(ret, h * (i-sidx-1));
}
index.push_back(i);
}
return ret;
}
这道题还有纯粹的DP解法,但是和今天的主题——单调栈无关,这里就不展开讨论了。
最后再总结一下单调栈。单调栈这种数据结构,通常应用在一维数组上。如果遇到的问题,和前后元素之间的大小关系有关系的话,(例如第一题中我们要找比某个元素大的元素,第二个题目中,前后的bar的高低影响了最终矩形的计算),我们可以试图用单调栈来解决。在思考如何使用单调栈的时候,可以回忆一下这两题的解题套路,然后想清楚,如果使用单调栈,每个元素出栈时候的意义。最后的时间复杂度,因为每个元素都出栈入栈各一次,所以是线性时间的复杂度。
转载出自于:https://zhuanlan.zhihu.com/p/26465701