让我们从三个方面来认识单调栈
1.什么是单调栈?
顾名思义,就是栈中的元素按照一定规则排列。例如:指定一个栈,栈中的元素自底向上是升序的,或者是降序的,栈中的所有元素按照规定好的单调性来组织。
2.单调栈有什么用?
最基本的,以数组为例,它可以帮助你找到第i个元素左右两边离其最近,且比他大或小的元素在哪;或者是帮你维护求解答案的可能性。
3.如何使用单调栈?
我们可以规定单调栈中元素压栈的规则,来找到左右两边离当前元素最近且比它大或小的元素位置。
如果我们是设定单调栈的压栈规则为“小压大”,那么使用单调栈就可以找到一个元素左右两边离它最近且比它大的元素在哪,如下图所示。如果单调栈的压栈规则为“大压小”,同理,我们使用单调栈就可以找到一个元素左右两边离它最近且比它小的元素在哪。
题目:739. 每日温度 - 力扣(LeetCode)
题目描述:
问题分析:
(1)题目要求每个温度的下一个更高的温度出现在几天后,那么不就是求右边离当前温度最近且比它大的温度在哪吗。
(2)知道下一个更高温度后,直接下一个更高温度的索引 - 当前温度的索引就是题目所要求的答案。
Code:
class Solution {
public static int MAX = 100001;
public static int [] stack = new int[MAX];
public static int size;
public int[] dailyTemperatures(int[] temperatures) {
size = 0;
int n = temperatures.length;
int [] ans = new int[n];
for(int i = 0;i < n;i ++){
while(size > 0 && temperatures[stack[size - 1]] < temperatures[i]){
int top = stack[--size];
ans[top] = i - top;
}
stack[size++] = i;
}
return ans;
}
}
补充说明:
若最后仍有温度存在于栈中,说明栈中的这些温度右边没有比它们高的温度,根据题目意思要将这些温度的结果赋值成0,但数组初始化后,每个索引上的元素值默认为0,所以在温度数组遍历完后,无需关心这些仍存在于栈中的温度。
题目:42. 接雨水 - 力扣(LeetCode)
题目描述:
问题分析:
这道题其实有很多种解法,但本文只着重于单调栈的解法!!!
(1)根据题目我们可以做出一种假设:每个柱子所能接的雨水由左边离它最近且比它高的柱子以及右边离它最近且比它高的柱子共同决定。
以上图的索引0、索引2、索引11处为例。索引0处的柱子高度为0,左边没有比它高的柱子(数组越界处),所以索引0处接不了雨水;索引1处的柱子高度为1,左边离他最近比它高的柱子在索引1处、右边离他最近且比它高的柱子在索引3处,那么就可以计算当前柱子能接的雨水;索引11处柱子的高度为1,虽然左边有比它高的,但右边没有比它高的柱子(数组越界处),所以索引11处也接不了雨水。
(2)基于上述假设,能接的总雨水量就是每个柱子接的雨水之和。
思路:
(1)既然是要找离当前柱子最近且比当前柱子高的柱子,那么我们就可以定义单调栈的入栈要求为:小压大,当即将入栈的柱子不符合单调栈的要求时,就可以让栈顶的柱子出栈,并更新栈顶柱子的答案。(让栈顶柱子出栈的柱子就是右边离他最近且比它高的柱子,栈顶柱子压着的柱子就是左边离他最近且比它高的柱子)
(2)如果栈中只有栈顶这一个元素,说明左边没有离栈顶元素最近且比它高的柱子,不用更新答案,直接退出循环。
(3)不断让栈顶元素出栈,直至入栈元素符合小压大的入栈要求。
Code:
class Solution {
public static int MAX = 100001;
public static int [] stack = new int[MAX];
public static int size;
public int trap(int[] height) {
size = 0;
int n = height.length;
int ans = 0;
for(int i = 0;i < n;i ++){
while(size > 0 && height[stack[size - 1]] <= height[i]){
int mid = stack[--size];
if(size == 0)
break;
int h = Math.min(height[i],height[stack[size - 1]]) - height[mid];
int w = i - stack[size - 1] - 1;
ans += (h * w);
}
stack[size++] = i;
}
return ans;
}
}
补充说明:
如果最后单调栈中仍留存有柱子,不用担心它会影响最终结果,因为当没有柱子让栈中的柱子出栈时,表明在栈中的这些柱子右边没有离它们最近且比他们高的柱子,它们接不了雨水,无需计算答案。
以上两题都是关于单调栈的经典用法:找到当前元素左右两边离它最近且比它大/小的元素,接下来通过另外一道题,引入单调栈的另一个用法:维护求解答案的可能性。
题目:962. 最大宽度坡 - 力扣(LeetCode)
题目描述:
问题分析:
(1)题目对于坡(i,j)有两个要求,一是i要严格小于j,一个是nums[i] <= nums[j],那我们可以用单调栈维护i的可能性,规定单调栈的入栈要求为“小压大”,不符合则不入栈。
让我们看上图。当索引4位置想要入栈时,不让它入栈,因为索引3位置在栈顶,它的值比索引4要小,索引4入栈就破坏了单调栈要求的“小压大”原则。为什么要求单调栈是“小压大”呢?是依据题目要求设计的,如果后面有一个j位置能与索引4位置组成一个坡,那么j位置必然能与索引3位置组成一个坡,因为索引3位置比索引4位置更小。但题目要求的是最大宽度坡,所以索引4位置入栈没有任何意义。
索引1位置和索引2位置为什么能入栈?假设索引1位置和索引2位置的大小分别为9、7,数组有没有可能是[9,7,9,9,9,9]?有可能啊,那就只有索引1位置能组成坡;数组有没有可能是[9,7,7,7,7]?也有可能,那么就只有索引2位置能组成坡。所以这就是为什么这两个位置能入栈的原因,这也贴合了单调栈的作用:维护求解答案的可能性。
(2)从头到尾遍历一次数组,将所有可能为i的解加入单调栈中;再从尾到头遍历一次数组,每次循环中如果当前元素大于等于栈顶元素就让其出栈,并更新一次结果,直至栈顶元素大于当前元素(贪心)。
Code:
class Solution {
public static int MAX = 500001;
public static int [] stack = new int[MAX];
public static int size;
public int maxWidthRamp(int[] nums) {
size = 1;
int n = nums.length;
for(int i = 1;i < n;i ++)
if(size > 0 && nums[stack[size - 1]] > nums[i])
stack[size++] = i;
int ans = 0;
for(int j = n - 1;j >= 0;j --){
while(size > 0 && nums[j] >= nums[stack[size - 1]]){
int top = stack[--size];
ans = Math.max(ans,j - top);
}
}
return ans;
}
}
补充说明:
初始化单调栈的长度size = 1是因为默认索引0处的元素一定会被放入栈中,我们从索引1处开始将可能为i位置的解放入单调栈。