单调栈是这样一个栈,它里面的元素从栈底到栈顶依次递减。
生成算法:
假如我们有一个数组nums[n]=[4,2,0,3,2,5],和一个空的栈stack。
我们遍历数组,对遇到的每一个元素num:
性质:
由上面的算法可知,单调栈有以下几条性质(分别用bottom, top, pop, num表示栈底元素4、新的栈顶元素3、旧的栈顶元素2、栈外元素等着进栈的元素5):
下面是栈的变化情况:
4 | |||||
---|---|---|---|---|---|
4 | 2 | ||||
4 | 2 | 0 | |||
4 | 2 | ||||
4 | 3 | ||||
4 | 3 | 2 | |||
4 | 3 | ||||
4 | |||||
5 |
实现:
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < nums.length; i++){
int num = nums[i];
//注意下面的写法,出栈要用循环
while(!stack.empty() && num > nums[stack.peek()]){
stack.pop();
}
stack.push(i);
}
既然是单调的,基本上遇到的问题就是和曲线的上升下降有关的,比如求极值(连续入栈时的第一次,连续出栈时的第一次),以及更多地,与极值(最值)有关的一些算法问题。
美学区间问题:
如果对于区间[Si,Sj] (1<=i
如果存在美学区间,那么如果任意区间的长度都小于等于k,那么输出最大的长度,否则输出最大长度比k大多少(MaxLength-k)。
解法:
利用单调栈,我们可以很轻松求解。观察这两个子数组和在单调栈中的特点发现,只要是出现num大于top的,那么num就和top构成一个美学区间。此时,只需要记录num和top的最远距离即可。
牛视野问题:
有一群牛站成一排,每头牛都是面朝右的,每头牛可以看到他右边身高比他小的牛。给出每头牛的身高,要求每头牛能看到的牛的总数。
类似的还有:给一个数组,返回一个大小相同的数组。返回的数组的第i个位置的值应当是,对于原数组中的第i个元素,至少往右走多少步,才能遇到一个比自己大的元素(如果之后没有比自己大的元素,或者已经是最后一个元素,则在返回数组的对应位置放上-1)。
解法:
观察上面的折线图,每个点相当于一头牛,它能往右边看到多少取决于右边第一个比它高的牛,利用单调栈。每当出现一头比栈顶还高的牛,说明这是栈顶牛第一次遇到的比它自己高的牛,栈顶牛会被当前牛挡住自己的视线,因此此时就需要计算他俩之间的牛有几头,这就是栈顶牛可以看到的牛的个数。
通过这两个例子,我们可以看到,单调栈的特点就是,栈顶元素遇到的第一个比自己大的元素就是将自己“赶出”栈的那个元素,谁把你赶出栈,谁就是你的“祖宗”,基本上的题目都是围绕着这个“祖宗”考察的。
下一个更大元素1
给定两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集(子集的意思是nums1中元素相对顺序有可能发生变化)。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
示例 1:
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
对于num1中的数字4,你无法在第二个数组中找到下一个更大的数字,因此输出 -1。
对于num1中的数字1,第二个数组中数字1右边的下一个较大数字是 3。
对于num1中的数字2,第二个数组中没有下一个更大的数字,因此输出 -1。
解法:
已经提示的很明显了,右边第一个比x大的元素,这正是单调栈的特点。还是那句话,就看谁把你“赶出来”的,谁就是你的下一个更大的元素。类似的还有下一个更大元素2。
单调栈的另一个特性:
前面所说的都是求数组中元素右边的第一个比它更大的元素,即把栈顶元素赶出栈的那个。
我们来看一下左边,数组中元素的左边最大的元素是什么?注意区别,区别在于两点,其一,最大;其二没有说是第一个(废话了,最大值是唯一的,不考虑有重复)。
观察栈中情况可以很容易发现,这个最大的数就是栈顶元素。观察上图,对于4,后面的2,0,3,2在出栈的时候栈底元素都是4,并且她们左边的最大元素显然是4。这个东西其实画个折线图很容易理解。
有时候要求右边最大,可以将数组元素逆序遍历。
看一个问题:接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这 种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。
计算雨水的方式决定了我们会采用单调栈的哪一个特性:
class Solution {
public int trap(int[] height) {
int sum = 0;
int[] leftMax = new int[height.length];
int[] rightMax = new int[height.length];
int max = -1;
//计算每个柱子左边得最高得柱子
for (int i = 0; i < height.length; i++){
leftMax[i] = max;//如果左边没有柱子,那他左边得最大值是-1.
if(height[i] > max) max = height[i];
}
max = -1;
//计算每个柱子右边最高的柱子
for (int i = height.length - 1; i > -1; i--){
rightMax[i] = max;
if(height[i] > max) max = height[i];
}
//计算每个柱子上面得雨水柱
for (int i = 0; i < height.length; i++){
int h = Math.min(leftMax[i], rightMax[i]);
if(h > height[i]){
sum += (h - height[i]);
}
}
return sum;
}
}
作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/trapping-rain-water/solution/zhu-zi-shang-mian-de-yu-shui-de-ji-suan-fang-fa-by/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
class Solution {
public int trap(int[] height) {
int sum = 0;
Stack<Integer> stack = new Stack<>();//单调栈
for (int i = 0; i < height.length; i++){
while(!stack.empty() && height[i] >= height[stack.peek()]){//出栈条件
int pop = stack.pop();
if(!stack.empty()){//计算容量
int top = stack.peek();
sum += (i - top - 1) * (Math.min(height[top], height[i]) - height[pop]);
}
}
stack.push(i);
}
return sum;
}
}
接雨水的问题是个难题,是因为还有一个更巧妙的办法,但是如果我们能够想到单调栈的解法,也足够了.
双指针法:
双指针法其实是竖着计算的进一步优化.根据竖着算的思路,计算一个柱子上方的雨水量,我们需要知道该柱子左边最高的柱子和右边最高的柱子二者之间的最小值.那么也就是说其实我们最终只需要的是它们两者之间的最小值,如果能想到一个办法,不必将两者都求出来,也即只求出来最小的那个.
这就是双指针的思想,两个指针分别从左和从右开始遍历,谁小谁就先走,这样的话,先找到的那个最值一定是左右两边最值的最小值了.
实现:
class Solution {
public int trap(int[] height) {
int sum = 0;
int left = 0, right = height.length-1, leftMax=-1, rightMax = -1;
while(left < right){
if(height[left] < height[right]){
if(height[left] < leftMax) sum += leftMax - height[left];
else leftMax = height[left];
left++;
}else{
if(height[right] < rightMax) sum += rightMax - height[right];
else rightMax = height[right];
right--;
}
}
return sum;
}
}
作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/trapping-rain-water/solution/shuang-zhi-zhen-fa-xu-yao-dui-jie-fa-2you-shen-ke-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
class Solution {
public int largestRectangleArea(int[] heights) {
if(heights.length == 0) return 0;
int max = 0;
for(int i = 0; i < heights.length; i++){
int l = i;
while((l-1) > -1 && heights[l-1] >= heights[i]) --l;
int r = i;
while((r+1) < heights.length && heights[r+1] >= heights[i]) ++r;
max = Math.max(max, (r-l+1)*heights[i]);
}
return max;
}
}
但是对于找边界这个工作,可以交给我们的单调栈,不过这次是找左边(右边)第一个更小的值,然后间接地找到边界。
以第3个柱子为例,它左边第一个更小的柱子是第2个,左边界是它右边的柱子,即第三个(它自己);右边第一个更小的柱子是第五个,右边界是第四个,如下图所示。
找到边界以后,面积为: ( r i g h t − l e f t + 1 ) ∗ h e i g h t i (right - left + 1) *height_{i} (right−left+1)∗heighti
heighti,right,left分别是当前柱子高度,右边界和左边界,因为我们使用的是单调栈,求出来的实际上不是边界,而是边界往右或左多了一位,因此对上面的公式进行修正: ( r i g h t − l e f t − 1 ) ∗ h e i g h t i (right - left - 1) *height_{i} (right−left−1)∗heighti
heighti,right,left分别是当前柱子高度,右边第一个更小的值的索引和左边第一个更小的值的索引。
实现:
class Solution {
public int largestRectangleArea(int[] heights) {
// if(heights.length == 0) return 0;
Stack<Integer> stack = new Stack<>();
stack.push(-1);
int max = 0;
for(int i = 0; i < heights.length; i++){
while(stack.size() > 1 && heights[i] < heights[stack.peek()]){
int cur = heights[stack.pop()];
int top = stack.peek();
max = Math.max(max, (i - top - 1)*cur);
}
stack.push(i);
}
while(stack.size() > 1){
int cur = heights[stack.pop()];
int top = stack.peek();
max = Math.max(max, (heights.length - top - 1)*cur);
}
return max;
}
}
作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/dan-diao-zeng-zhan-xiang-by-fang-wen-chu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
class Solution {
public int[] dailyTemperatures(int[] T) {
//典型的单调栈问题
Stack<Integer> stack = new Stack<>();
int[] daily = new int[T.length];
for(int i = 0; i < T.length; i++){
while(!stack.empty() && T[i] > T[stack.peek()]){
//出栈时计算天数
int top = stack.pop();
daily[top] = i - top;
}
//进栈的是下标
stack.push(i);
}
//最后还留在栈中的都是后面没有温度超过它,因此置0.
while(!stack.empty()){
daily[stack.pop()] = 0;
}
return daily;
}
}
作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/daily-temperatures/solution/dan-diao-zhan-by-fang-wen-chu-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复杂度: 时间复杂度为O(n),因为只需要遍历一遍数组;空间复杂度为O(n),因为栈最大时是温度呈下降趋势,此时栈大小为n,并且返回值也需要空间。