最近在学习数据结构和算法相关内容,非科班出身的我,也只能慢慢地,一步一个脚印的学习、理解基本概念,很多笔记的内容来源于网上,文末也附上相应的链接,自己根据网上的博客内容以及自己的理解写出以下内容。
1. 什么是单调栈
在此之前,我们应该知道,什么是栈,一种先进后出的数据结构(存储数据的一种方式),只从数据集的一端进、出数据。而单调栈就是栈中的数据的排列具有单调性,分为单调递增栈和单调递减栈。需要注意的是,不要理解为一个单调栈是将原来栈中的数据进行递增排序和递减排序后形成新的栈。如下图,分别插入6,10,3,7,4,12的时候,单调递增栈和单调递减栈的情况:
上图已经很清楚介绍了单调栈是什么样子的。
2. 单调栈该怎么用呢
单调递减栈能表示入栈元素左边第一个比它大的元素;
单调递增栈能表示入栈元素左边第一个比它小的元素
我们拿上图的单调递减栈来举例,理解上面的两句话,其中源栈数据序列为:[6, 10, 3, 7, 4, 12],
当前元素 | 单调栈数据 | 查找比自己大的元素 | 说明 |
---|---|---|---|
6 | [] | 无 | 栈为空,说明其左边没有比它大的元素,然后6入栈,单调栈为[6] |
10 | [6] | 无 | 栈顶元素6比自己(10)小,为了维持单调递减,6出栈,10入栈,单调栈为[10] |
3 | [10] | 10 | 3比10小,直接入栈,单调栈为[10,3] |
7 | [10, 3] | 10 | 7比3大,为了不保证递减,3出栈,7入栈,单调栈为[10,7] |
4 | [10, 7] | 7 | 4比7小,直接入栈,单调栈为[10,7,4] |
12 | [10,7,4] | 无 | 12比栈里所有元素都大,弹完后栈空,找不到比自己大的,单调栈为[12] |
那么同理,在递增栈中,可以查找第一个小元素。
实现代码如下:
def getLeftMinNum(src):
"""
获取左边第一个小于自己的数,构造一个单调递增栈
"""
monotoneStack, res = [], [0 for i in range(len(src))]
for i in range(len(src)):
if len(monotoneStack) == 0:
monotoneStack.append(src[i])
res[i] = -1 # 如果左边没有比自己小的设置为-1
else:
while len(monotoneStack) > 0 and src[i] < monotoneStack[-1]:
monotoneStack.pop()
if len(monotoneStack) > 0:
res[i] = monotoneStack[-1]
else:
res[i] = -1
monotoneStack.append(src[i])
print(res)
return monotoneStack
def getLeftMaxNum(src):
"""
获取左边第一个大于自己的数,构造一个单调递减栈
"""
monotoneStack, res = [], [0 for i in range(len(src))]
for i in range(len(src)):
if len(monotoneStack) == 0:
monotoneStack.append(src[i])
res[i] = -1 # 如果左边没有比自己大的设置为-1
else:
while len(monotoneStack) > 0 and src[i] > monotoneStack[-1]:
monotoneStack.pop()
if len(monotoneStack) > 0:
res[i] = monotoneStack[-1]
else:
res[i] = -1
monotoneStack.append(src[i])
print(res)
return monotoneStack
if __name__ == '__main__':
tmp = [6, 10, 3, 7, 4, 12]
getLeftMinNum(tmp)
getLeftMaxNum(tmp)
3.例题
在第二节中介绍了可以通过单调栈获取元素前一个最大值与最小值,那么在实际中该如何应用呢?我们结合几个小问题来应用它!
3.1 BadHairDay
原题目链接:http://poj.org/problem?id=3250
Some of Farmer John’s N cows (1 ≤ N ≤ 80,000) are having a bad hair day! Since each cow is self-conscious about her messy hairstyle, FJ wants to count the number of other cows that can see the top of other cows’ heads.
Each cow i has a specified height hi (1 ≤ hi ≤ 1,000,000,000) and is standing in a line of cows all facing east (to the right in our diagrams). Therefore, cow i can see the tops of the heads of cows in front of her (namely cows i+1, i+2, and so on), for as long as these cows are strictly shorter than cow i.
(农夫约翰的一些奶牛(1≤N≤80,000)今天的毛很糟糕!由于每头奶牛都对自己凌乱的发型感到难为情,FJ想数一数能看到其他奶牛头顶的奶牛数量。
每头奶牛i都有一个指定的高度hi(1≤hi≤1000,000,000),并且站在一排面向东(在我们的图中右侧)的奶牛中。因此,我可以看到奶牛的头顶在她的面前(即奶牛i+1,奶牛i+2,等等),只要这些奶牛严格小于奶牛i。)
每头牛只能看见右边牛的头,根据上面的例子,我们假设牛的高度分别为:[10,3,7,4,12,2],那么对于牛则有:
index | 牛的高度 | 当前牛能看到牛的高度 | 数量 |
---|---|---|---|
0 | 10 | 3,7,4 | 3 |
1 | 3 | null | 0 |
2 | 7 | 4 | 1 |
3 | 4 | null | 0 |
4 | 12 | 2 | 1 |
5 | 2 | null | 0 |
那么我们的思路就是:找到右边第一个比自己高的牛的index,然后index -1 就是当前牛能够看到最远的那头牛。这样,我们的问题就简化为找到右边一个比自己高的牛,那么也就是单调递减栈的功能(左边比自己大)。在实际程序中,我们采用逆向扫描的方式,这是你需要转变思想的,逆序从最后一个开始(那么就是原来的左)进行比较。
程序实现如下:
def badHair(cows):
# minIndexStack保存对应高度的索引
minIndexStack, result, i = [], 0, len(cows) - 1
while i >= 0:
# 当前值与单调栈中栈顶的值进行比较
while len(minIndexStack) > 0 and cows[i] > cows[minIndexStack[-1]]:
minIndexStack.pop()
# 如果栈里没有数据了,说明自己是最高的,可以看完整个队伍。
if len(minIndexStack) == 0:
bigNumIndex = len(cows)
else:
bigNumIndex = minIndexStack[-1]
minIndexStack.append(i)
result += bigNumIndex - i - 1 # -1是因为看不到最高的那个,所以需要把最高的刨除掉。
i -= 1
return result
if __name__ == '__main__':
tmp = [10,3,7,4,12,2]
print(badHair(tmp))
3.2 接雨水
原题链接:https://leetcode-cn.com/problems/trapping-rain-water/(困难)
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
题目分析:积水只有在左右大中间小的情况下会形成。正常的思路下,我们只要找到两边比较多就可以找到积水,但是如果未来出现更高的台阶,我们必须回过头来看之前的计算是不是有效的。换种思路,一个水潭可以由底层的加上高层的组成,如下图:
而且这两个可以不影响,我们可以先得到浅蓝色的部分,后面如果出现更高的再加上深蓝色的部分。如果不出现就不加。
我们同样使用单调递减栈,我们知道栈有两个操作,入栈和出栈。单调栈的出入栈表示:
- 入栈,表明本身比栈顶小,说明在下台阶,下台阶是行程不了积水的。
- 出栈,表明本身比栈顶大,肯定会形成积水。
所以每次计算积水肯定是在pop的时候计算。而且栈里最少有两个元素的时候才会形成,因为最小的积水也是有两个边和一个坑组成的,最少也是栈里两个加上刚来的一个。
这里我们可以想象成一个木桶,根据木桶理论,容量由最低的那块木板决定,所以桶的容量需要由 长木板+桶底+短木板共同决定
我们看上面图的例子:
遍历到3的时候。栈:[1, 0],来了一个2,2比0大,0要出栈,这个时候就可以知道1和2中间夹了一个0,找1和2最小值,短木板是1,桶底是0,所以宽度是1,高度是1,得到面积是1.
遍历到6的时候。栈:[2,1,0],来了1,所以1和1中间夹了0,同样得到面积是1,得到的是浅蓝色的部分。
继续往后遍历来到7,来了一个3,栈:[2,1] (看入栈的逻辑,栈里也可以是[2,1,1],相同的1可以入可以不入)。假设是[2,1],先弹1,短木板是2,长木板是3,桶底是1,高度是1,宽度index=6-3=3,所以面积是3.
代码实现:
def trap(height):
if len(height) == 0: return 0
sumArea, stack = 0, []
for right in range(len(height)):
while len(stack) > 0 and height[stack[-1]] <= height[right]:
if len(stack) >= 2:
j = stack.pop()
left = stack[-1]
# 水高,就像一个木桶:得到最低的木板减去底得到能装水的高度
waterHeight = min(height[right], height[left]) - height[j]
waterWidth = right - left - 1
sumArea += waterHeight*waterWidth
else:
stack.pop()
stack.append(right)
return sumArea
if __name__ == '__main__':
tmp = [0,1,0,2,1,0,1,3,2,1,2,1]
print(trap(tmp))
Reference
个人订阅号