单调栈
适用场景:
单调栈最常见的场景就是:给定一个序列,求在这个序列中每个数左边/右边离它最近且小于/大于它的数。
这是用二分无法解决的问题,因为整数二分要求序列必须具有单调性。
基本原理:
考虑的方式其实是和双指针类似的,先想一下暴力做法,然后挖掘一些性质,可以使得我们把目光集中在比较少的状态里边,从而起到把时间复杂度降低的这样一个效果。
如果第二个点比第一个点小且第二个点比第一个点更靠近右边,那么单调栈就删除第一个点,这样的话删除掉所有逆序的点就可以得到一个严格单调上升的序列(也就是哪些元素永远不会被输出出来,那么就删掉它),答案就在这个栈里边找。
模板
#在python中,栈可以用内置数据结构列表来模拟
for i in range(n):
x=int(input())
#这里的条件stack[-1]>=x,也就是栈尾与当前值的比较我们可以这样考虑
#如果当前栈尾元素大于等于x,并且栈尾中的元素比x更靠近左边,那么显然x比stack[-1]更符合要求,那么x就应该进栈,而栈尾就要进行一次弹出,此时栈尾中的元素就扮演着逆序点的角色
#比如说a[3]>=a[5],那么a[3]就不会被作为答案输出出来,因为a[5]比它更靠近右边,而且数值比它小
while len(stack) and stack[-1]>=x:
stack.pop()
if len(stack):
#因为单调栈中的元素是严格单调的,所以一般栈尾中元素是最有用的
print(stack[-1])
#记得把x添加进去
stack.append(x)
例题
acwing.830.单调栈
题目描述:
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入格式:
第一行包含整数 N,表示数列长度。
第二行包含 N 个整数,表示整数数列。
输出格式:
共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。
数据范围:
1 ≤ N ≤ 10的5次方
1 ≤ 数列中元素 ≤ 10的9次方
代码实现:
N=int(input())
a=list(map(int,input().split()))
stack=[]
for i in range(N):
while len(stack) and stack[-1]>=a[i]:
stack.pop()
if len(stack):
print(stack[-1],end=' ')
else:
print(-1,end=' ')
stack.append(a[i])
原题链接: link
单调队列
单调队列其实和单调栈的实现方式差不多,不过顾名思义,一个是用栈实现,一个是用队列实现。
适用场景:
单调队列的适用场景非常有限,一个手能数过来的那种,通常有以下两种情形需要使用到单调队列。
- 求窗口中的最值
- 找到距离某元素最近的大于/小于该元素的元素,这一点跟单调栈是一样的,这是因为单调栈和单调队列都满足严格的单调性,那么它们最有用的元素就应该在头或尾,而头或尾的这个值就是最值。
基本原理:
①用普通队列怎么做
②将队列中没有用的元素删掉—使其具有了单调性
③可以用O(1)的时间从队头或队尾取出目标元素
例题
acwing.154.滑动窗口
题目描述:
给定一个大小为 n≤10的6次方的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7],k为 3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 [5 3 6] 7 | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。 |
输入格式:
输入包含两行。
第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式:
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
代码实现:
#如果后边的元素比前边的元素小,那么势必前边的元素不会被输出出来,也就是说这个元素就无用了,可以删掉
n,k=map(int,input().split())
a=list(map(int,input().split()))
que=list([0 for i in range(1000011)])
hh,tt=0,-1
for i in range(n):
#由于在出队时,不仅有元素从队头出了,也有元素从队尾出了,所以这是一个双端队列
#这里的出队列并不是真的出了,只是说这些下标对应的元素无效了
#队列中存储的是下标
#维护窗口大小
if hh<=tt and i-k+1>que[hh]:
hh+=1
while hh<=tt and a[que[tt]]>=a[i]:
tt-=1
tt+=1
que[tt]=i
#当队列长度大于等于k时再输出
if i>=k-1:
print(a[que[hh]],end=' ')
print('')
#重置,之前存在的元素会被覆盖掉,所以不用考虑初始化这个问题
hh,tt=0,-1
for i in range(n):
if hh<=tt and i-k+1>que[hh]:
hh+=1
while hh<=tt and a[que[tt]]<=a[i]:
tt-=1
tt+=1
que[tt]=i
#当队列长度大于等于k时再输出
if i>=k-1:
print(a[que[hh]],end=' ')
我个人觉得单就用python实现滑动窗口这道题而言,单调队列和单调栈最大的区别在于,单调栈中一直在进栈和出栈的过程中,而单调队列并不是真正意义上的进出元素,因为无论是用内置函数还是列表构造队列,从队头出元素都是一个比较难解决的问题,这样的话使用Queue库中的队列感觉不比列表有优势,所以不如用列表模拟, 空间开大点,使用双指针完成对队列有用数据的维护。
原题链接: link