目录
一、什么是单调栈
单调栈是一种特殊的栈,它的特点是栈中的元素始终保持单调有序。通常有两种单调栈,分别是单调递增栈和单调递减栈。
单调递增栈顾名思义,栈内元素从栈底到栈顶递增有序,即栈顶元素最小,栈底元素最大。而单调递减栈则相反,栈内元素从栈底到栈顶递减有序,即栈顶元素最大,栈底元素最小。
这种利用单调栈求解问题的思路通常可以简化算法,提高算法的效率。在解决问题时,我们需要仔细观察问题的性质,判断是否适用单调栈。如果问题符合单调栈的特点,我们就可以借助单调栈来解决问题,以达到优化算法效率的目的。
简单的说,单调栈就是通过维护一个栈,来达到以空间换时间,从而使得时间复杂度从O(n^2)降低到O(n),大大提高算法效率。
二、单调栈的应用场景
单调栈的主要应用场景是求解某个元素的左边或者右边第一个比它大或者小的元素。以求解元素的左边第一个比它小的元素为例,我们可以从左到右遍历数组,将每个元素入栈。在入栈的过程中,我们可以比较栈顶元素与当前元素的大小,如果栈顶元素大于当前元素,则栈顶元素比当前元素的左边第一个比它小的元素。将栈顶元素出栈后,继续比较栈顶元素与当前元素的大小,直到栈为空或者栈顶元素小于当前元素。这样,我们就可以得到当前元素的左边第一个比它小的元素。
三、单调栈算法解题的一般步骤
需要着重进行解释的步骤如下:
1、定义一个栈
Deque<Integer> deque=new LinkedList<>();
这里一般会用到Deque的以下四个方法:
deque.isEmpty():如果deque不包含任何元素,则返回true,否则返回false。因为要栈顶元素在满足要求的时候要弹出,所以需要进行空栈判断。有些场景,可能栈一定不会空的时候,就不需要该方法进行空栈判断。
deque.push(e):将元素e入栈。一定能用到该方法。
deque.pop():将栈顶元素弹出,并返回当前弹出的栈顶元素。一定能用到该方法。
deque.peek():获取栈顶元素,不弹出。一定能用到该方法。
2、确定栈里存放的元素是什么
一般情况下单调栈里面存放的都是目标数组的下标,这样做的好处就是,我们不仅知道遍历过的元素下标,而且也可以通过下标找到对应数组里面的元素。当然,当前单调栈里面具体是存下标还是实际的数据值,取决于具体的应用场景和算法需求。毕竟,离开业务场景,谈技术就是扯淡!
3、确定单调栈是递增还是递减
如果求一个元素右边第一个更大元素时,单调栈就是递增的,如果求一个元素右边第一个更小元素,单调栈就是递减的。
4、遍历目标数组,分情况进行处理
这里在遍历目标数组的时候,需要比较当前遍历元素和栈顶元素的大小。
这个时候就有如下三种情况:
■ 当前遍历的元素小于栈顶元素的情况
■ 当前遍历的元素等于栈顶元素的情况
■ 当前遍历的元素大于栈顶元素的情况
一般情况下单调递增栈在当前遍历的元素大于栈顶元素的情况下进行主要的逻辑处理,其他情况下直接把当前元素做入栈操作;而单调递减栈在当前遍历的元素小于栈顶元素的情况下进行主要的逻辑处理,其他情况下直接把当前元素做入栈操作。
一般代码:
deque.push(0);
for(int i=1;i<lens;i++){
//单调递减栈这里就是大于,即nums[i]>nums[deque.peek()]
if(nums[i]<nums[deque.peek()]){
stack.push(i);
}else if(nums[i]==nums[deque.peek()]){
stack.push(i);
//此处除了入栈,在有些场景下,还有可能有其他操作
..............
} else{
//循环比较,直到遇到当前元素小于栈顶的元素情况,跳出循环
//单调递减栈,这里是小于,即nums[i]<nums[deque.peek()]
while(!deque.isEmpty()&&nums[i]>nums[deque.peek()]){
//主要逻辑
............
...........
//弹出栈顶元素
deque.pop();
}
stack.push(i);
}
}
上述代码可以简化成如下代码:
deque.push(0);
for(int i=1;i<lens;i++){
//循环比较,直到遇到当前元素小于栈顶的元素情况,跳出循环
//单调递减栈,这里是小于,即nums[i]<nums[deque.peek()]
//因为while循环里面已经做条件判断了
while(!deque.isEmpty()&&nums[i]>nums[deque.peek()]){
//主要逻辑
............
...........
//弹出栈顶元素
deque.pop();
}
stack.push(i);
}
注意:在对栈进行操作的时候,要尽量减少对栈的操作次数。比如,如果在获取栈顶元素后,还要进行弹出操作且再获取栈顶元素后不需要在将该元素保留栈顶。则此时,可以直接使用pop()方法,代替使用peek()方法后再使用pop()方法。
四、力扣例题
以力扣42题接雨水为例。
1、题目描述
给定
n
个非负整数表示每个宽度为1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5] 输出:9
提示:
n == height.length
1 <= n <= 2 * 104
0 <= height[i] <= 105
2、解体思路
我们需要明确的是,在什么情况下可以接到雨水?
观察示例1的图,我们不难看出,水都是积在凹槽处的 。也就是在当前元素小于于右边第一个元素和当前元素大于左边第一个元素之间,是可以有雨水的。这其实也就是找当前元素右边第一个大于它的元素,这不就是我们的单调递增栈能够做的事吗?但是,这里可能会有一个疑问,那怎么找当前元素左边第一个大于它的元素呢?其实,仔细想想便不难得知,因为我们使用的是单调递增栈,所以栈顶元素的前一个元素也就是左边第一个大于它的元素。
■ 确定栈里存放的元素是什么?
我们使用单调栈也是通过长*宽来计算出雨水面积的。而我们要计算长,便是通过数组height里的数值来计算出来的;而宽则是要用下标来计算出来的。所以,此处栈里存放的元素应该是下标,因为通过下标,我们可以很方便的获取对应的数值,而通过数值,我们是不能在不增加新的数据结构的情况下,获取对应的下标的。
■ 确定是递增栈还是递减栈
通过上面的分析可以得知,本题是使用单调递增栈。当要入栈的柱子高度大于栈顶元素时,就出现凹槽了,栈顶元素就是凹槽的底部,栈顶的下一个元素就是凹槽左边的柱子,而待入栈的元素就是凹槽右边的柱子。
3、代码示例
class Solution {
public int trap(int[] height) {
int len = height.length;
int sum = 0;
Deque<Integer> deque = new LinkedList<>();
deque.push(0);
for (int i = 1; i < len; i++) {
while (!deque.isEmpty() && height[i] > height[deque.peek()]) {
int p1 = deque.pop();
if (!deque.isEmpty()) {
int p2 = deque.peek();
// 高取两边最小的一个
int h = Math.min(height[p2], height[i]) - height[p1];
int w = i - p2 - 1;
int area = h * w;
sum +=area;
}
}
deque.push(i);
}
return sum;
}
}
五、总结
单调栈算法,其实就是将遍历的元素都记录下来。所以,在遍历目标数组的时候就只需要遍历一遍,时间复杂度也就从O(n^2)降低到O(n)。当然,单调栈也是存在一些缺点的。比如,局限性较大,单调栈只适用于一些特定的问题,比如求下一个更大/更小元素、求最大矩形面积等。对于其他类型的问题,可能并不适用;无法优化:单调栈的算法实现基本上是固定的,无法进行太多的优化。因此,在某些特定的情况下,可能存在更优的解决方法。
注:力扣上其他单调栈算法题,84.柱状图中最大矩形;496.下一个更多元素I; 503下一个更大元素II; 739.每日温度;901.股票价格跨度。
如果对您有帮助,欢迎关注、点赞、收藏!