单调栈与单调队列

单调栈

适用场景:
单调栈最常见的场景就是:给定一个序列,求在这个序列中每个数左边/右边离它最近且小于/大于它的数。
这是用二分无法解决的问题,因为整数二分要求序列必须具有单调性。
基本原理:
考虑的方式其实是和双指针类似的,先想一下暴力做法,然后挖掘一些性质,可以使得我们把目光集中在比较少的状态里边,从而起到把时间复杂度降低的这样一个效果。
如果第二个点比第一个点小且第二个点比第一个点更靠近右边,那么单调栈就删除第一个点,这样的话删除掉所有逆序的点就可以得到一个严格单调上升的序列(也就是哪些元素永远不会被输出出来,那么就删掉它),答案就在这个栈里边找。

在这里插入图片描述

模板

#在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-13
1 [3 -1 -3] 5 3 6 7-33
1 3 [-1 -3 5] 3 6 7-35
1 3 [-1 -3 5] 3 6 7-35
1 3 -1 -3 [5 3 6] 736
1 3 -1 -3 [5 3 6] 737
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式:
输入包含两行。
第一行包含两个整数 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

so.far_away

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值