最近,博主在Leetcode刷到了一类单调栈的题目,经过一番总结和梳理,现在来这里分享给大家,请大家有序上车,前方到站“玩转单调栈”。
一.什么是单调栈?
栈大家都不陌生(如果读者还未掌握,请先去了解什么是栈再来看),其特点为LIFO(后入先出),而单调栈说到底还是一种栈,只不过它比一般的栈多了一个特性:在单调栈中,从栈顶到栈底的元素是严格递增/递减,前者称为单调递增栈(简称单增栈),后者称为单调递减栈(简称单减栈)。
说明:在上图中,左侧的为单增栈,右侧的为单减栈。
二.如何创建单调栈?
对于单调栈的创建其实也很简单,下面我会为大家手工展示单增栈/单减栈的创建,然后给出一个算法的伪代码模板。
2.1.单增栈的创建
核心步骤
在单增栈中,对于待入栈的元素a,首先我们要弹出栈中所有小于它的元素,然后将a入栈。
创建示意图
这里以序列[2,1,5,6,2,3]
为例子:
伪代码
void singleStack(lst):
创建一个初始为空的栈stack
N = length(lst) #获取待创建序列的长度
for i = 0 to N:
#弹出栈内所有小于待入栈元素的元素
while stack不为空 and stack.top() < lst[i]:
stack.pop()
stack.append(lst[i]) #待入栈元素入栈
打印当前栈stack
2.2.单减栈的创建
核心步骤
单减栈的创建与单减栈元素入栈的策略刚好相反,在单减栈中对于当前待入栈元素a,需要弹出栈内所有大于它的元素,然后将a入栈。
创建示意图
这里同样以序列[2,1,5,6,2,3]
为例:
伪代码
单减栈的创建伪代码和单增栈的基本一致,只不过需要把stack.top() < lst[i]
改为stack.top() > lst[i]
,这里也给出伪代码:
void singleStack(lst):
创建一个初始为空的栈stack
N = length(lst) #获取待创建序列的长度
for i = 0 to N:
#弹出栈内所有小于待入栈元素的元素
while stack不为空 and stack.top() > lst[i]:
stack.pop()
stack.append(lst[i]) #待入栈元素入栈
打印当前栈stack
三.单调栈有啥作用呢?
在了解了单调栈的含义和创建方法后,我们有必要谈谈单调栈的作用,只有了解这个我们才可以利用单调栈在特定的情形下解决问题。
3.1.单增栈的作用
对于单增栈,我们可以利用它来寻找一个序列中某个元素右边第一个比它大的元素(除非该元素右侧不存在比它大的元素),当然在栈中比它先入栈的元素一定比它大。
这里同样举个栗子,假设一个序列已经进入单增栈中的元素序列为[5,3,2,1]
,此时栈外待入栈元素为4,如下图所示:
由于是单增栈,因此需要从栈中逐个取出小于4的元素,取出1时,可以知道在序列中它右边比它大的第一个元素为4,它左边第一个比它大的元素为2,取出2时,可知序列中它右边第一个比它大的元素为4,它左边第一个比它大的元素为3,依次类推…;
3.2.单减栈的作用
对于单减栈,我们可以利用它找到一个序列中某个元素右边第一个比它小的元素(除非该元素右侧不存在比它小的元素),当然栈中比其先入栈的元素也一定比它小。
这里为了方便大家理解同样给出了栗子,假设对于一个序列,已经进入单减栈的元素为[1,2,3,5]
,待入栈元素为4,如下图所示:
由于是单减栈,因此需要从栈中逐个取出大于4的元素,而栈中只有一个元素5,大于4,因此取出5,此时栈顶元素为3,可得序列中5左边第一个比它小的元素为3,5右边第一个比它大的元素为4。
四.单增栈练习
这里以Leetcode中的问题:柱状图中的最大矩形(原题链接入口)为例。
4.1.题目
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。
示例:
输入: [2,1,5,6,2,3]
输出: 10
4.2.解题思路
对于该题,由于要能勾勒出矩形,肯定是要以一个连续矩形范围中的最低矩形为基准,例如对于柱2,其高度为1,因此包含它的最大矩形面积为6,如下图所示:
对于柱5,其高度为6,因此包含它的最大矩形面积为8,如下图所示:
根据上面的理解,对于本道题,只需要找到以每个柱形为最低点的最大连续柱状范围构成的矩形面积中的最大值即可。
4.3.如何寻找以某个柱形为最低点的最大连续柱状范围?
由于题目要找以某个柱形为最低点的范围,因此可以利用单减栈,利用单减栈我们可以找到某个元素右边第一个比它小的元素(确定范围的右边界),而对于栈内比它先入栈的元素肯定比它要小(确定范围左边界)。根据该特性,我们可以很容易的找到以某个柱形每个柱形为最低点的最大连续柱状范围。
4.4.利用单减栈的动态求解过程演示
这里采用原题的示例举个栗子,示例1求解的序列为[2,1,5,6,2,3]
,由于要求以每个元素为最低点的最大柱状范围,在该序列前后各加上一个0,即[0,2,1,5,6,2,3,0]
,通过这种方式使得利用单调栈处理时能够弹出并计算以每个为最低点最大连续柱形范围的面积。
步骤 | 操作 | 说明 |
---|---|---|
1 | 0入栈 | |
2 | 2入栈 | |
3 | 1待入栈,弹出2 | 以柱形1为最低点的最大连续柱状范围为:柱形1(高为2),其面积为2 |
4 | 1入栈 | |
5 | 5入栈 | |
6 | 6入栈 | |
7 | 2待入栈,弹出6 | 以柱形4为最低点的最大柱状范围为:柱形4(高位6),其面积为6 |
8 | 弹出5 | 以柱形3为最低点的最大连续柱状范围为:柱形3(高位5),柱形4(高位6),其面积为10 |
9 | 2入栈 | |
10 | 3入栈 | |
11 | 0待入栈,弹出3 | 以柱形6为最低点的最大连续柱状范围为:柱形6(高为3),其面积为3 |
12 | 弹出2 | 以柱形5为最低点的最大柱状范围为:柱形3(高位5)柱形4(高位6)柱形5(高位2)柱形6(高位3),其面积为8 |
13 | 弹出1 | 以柱形2为最低点的最大柱状范围为:柱形1(高位2)柱形2(高位1)柱形3(高位5)柱形4(高位6)柱形5(高位2)柱形6(高位3),其面积为6 |
以上计算出的面积值中最大值为10,因此题解为10。注意,实际入栈的是对应元素的索引,这样才方便寻找范围!!!
4.5.示例代码
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
stack = []
#使得每个元素都能够弹出
heights = [0] + heights + [0]
maxarea = 0
for i in range(len(heights)):
while stack and heights[stack[-1]] > heights[i]:
#最大连续柱形范围stack[-1] + 1 ~ i - 1
tmp = stack.pop()
maxarea = max(maxarea,(i- stack[-1] -1)*heights[tmp])
stack.append(i)
return maxarea
4.6.对于单调栈的其它练习
上述只是利用单调栈解决实际问题的冰山一角,这里给出Leetcode上一些关于单调栈的习题,有兴趣的可以去试试巩固一下单调栈:
五.结语
以上便是关于单调栈的全部内容,若有什么不对的地方可以在下方留言提出,博主以后会不断以这样专题的形式更新算法题解,有兴趣的可以关注一下,另外,码文不易,各位大佬点个赞吧!!!