单调栈,单调队列

单调栈,单调队列

单调栈

单调栈一般应用场景非常少,只有在一个数组中,找到每个元素左边离他最近的而且比他大或者比他小的元素

比如一个数组

3 4 2 7 5

输出每个数左边第一个比它小的数,如果不存在则输出-1

则输出为

-1 3 -1 2 2

这种情况就可以使用单调栈算法,其实完全可以换句话说,其实单调栈算法就是专门解决这个场景的

这个算法其实不太好记忆,或者说过现在觉得自己弄懂了,但是一段时间就忘了。

可以先考虑暴力做法怎么做:

无非就是设计一个栈,然后每遍历到一个元素,就在栈中遍历,找到第一个比他小的数,因为栈的先进后出的原则,那么后入栈的元素的肯定是会先遍历到,所以保证了找到的这个元素离当前的元素最近。这样遍历原数组和每次都会把整个栈遍历一次,所以是双重循环,时间复杂度为O(n^ 2),时间复杂度太高了。完全可以优化,使用单调栈算法。

这里先模拟一边单调栈算法的过程,首先肯定是遍历整个数组。想象成一个数一个数开始进入算法流程。

首先 是3 进入单调栈,因为前面没有比他小的元素 所以输出-1

输出 -1 栈:3

然后是4 ,比3大所以再次进入栈中,而且找到前面比他小的数是3,所以输出3

输出 3 栈:3 4

然后是2,这个时候就是单调栈算法特别之处了,他要搜索前面比他小的元素,但是不是只是搜索,而且把比他大获得等于他的元素全部干掉,3 4比他大直接出栈(因为3 4 比2早进入栈,而且又比2大,说明在后面的过程中,他肯定不会被输出,因为2在他后面而且比他小,永无出头之日,那么可以直接删掉)栈直接为空,因为没有比2小的元素,输出-1 2入栈,

输出-1 栈 2

然后遍历到7,向前搜索比他小的元素并干掉比他大或者等于他的的元素,那么就是2,输出,然后7入栈

输出2 栈 2 7

然后遍历到5 ,向前搜索比他小的元素并且干掉比他大或者等于他的元素,把7干掉,然后2比他小,输出,最后5入栈

输出2 栈:2 5

总而言之:单调栈算法的流程就是遍历每个元素,每个元素都在栈中向前搜索比他小的元素并且干掉比他大的元素,找到比他小的元素输出,然后自己入栈

所以就能写出代码

#include <iostream>
const int N = 10010;
int a[N];
int stk[N],tt;//定义一个栈,用数组模拟栈
int main(){
    int n ;
    cin >> n;
    for(int i = 0;i < n;i ++) scanf("%d",&a[i]);
    //单调栈的核心算法
    for(int i = 0;i < n;i ++) {
    	while(tt && stk[tt] >= a[i]) tt --;//在栈中向前寻找比他小的元素,然后让大于等于他的元素出栈
        if(tt) cout << st[tt] << " ";//只有两种情况会退出while循环,这是典型的出while循环判断,一种是!tt 说明没有元素比a[i]小 ,一种是站不为空,说明存在比他小的元素,直接输出就行
        else cout << -1 << " ";
    }
    return 0;
}

单调队列

单调队列使用的场景也是十分的稀少,基本只有一种,就是求一个滑动窗口中的最值,有一个数组和一个滑动窗口,滑动窗口剖i的

比如 最小值 最大值

[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

先考虑一下朴素做法该怎么做,就是维护一个双端队列,然后每次右移一位都遍历整个滑动窗口,然后得到他的最大最小值,这样明显时间复杂度是O(n ^ 2)

可以使用单调队列进行优化,假设现在求滑动窗口滑动过程中的最小值。

核心思路就是在一个新的数从队尾进入双端队列的时候,他将队列中比他大等于他的元素全部干掉,因为他们比他“旧”,所以在滑动的过程中肯定要比现在这个数更早出队,而且还不如这个数大,所以没有用 可以直接删掉,(这样保证了在双端队列中的元素都是单调的)同时注意每次滑动这个窗口的时候,都i应该判断队头元素是不是已经不在滑动窗口里面了。然后每次需按照这个对列中的最值就把队头元素输出就可以。

根据这个思路可以写出代码,注意在代码中双端队列存的是元素的下标而不是元素的值,因为我们要判断窗口滑动之后 队头元素是不是已经出队了

#include <cstdio>
#include <iostream>
const int N = 10010;
int a[N];
int q[N],hh,tt = -1;
int main(){
    int n,k;
    cin >> n >> k;
    for(int i = 0;i < n;i ++) scanf("%d",&a[i]);
    for(int i = 0;i < n;i ++) {
        while(i - k + 1 > q[hh]) hh ++;//判断队头元素有没有出队
        while(hh <= tt && a[q[tt]] >=  a[i]) tt --;//新元素开始将队列中大于等于他的元素淘汰
        q[++ tt] = i;//这个元素的下标入队
 		if(i + 1 >= k) printf("%d ",a[q[hh]]);//当窗口中的元素大于等于k的时候才进行输出
    }
    return 0;
}

总结:

可以看出单调栈和单调队列,的原理都十分类似,就是往栈或者队列中加入新元素的时候,先把比这个元素早进入栈或者队列中,而且比他更不符合条件的元素(更大或者更小或者是相等,看题目要求)淘汰。(有点类似现在的互联网行业),最后栈或者队列中的元素就都是单调的了,减少了大量的栈和队列中的元素,这样就将暴力算法进行了优化。

记忆口诀:单调栈:每个新元素从单调栈中栈顶元素开始从后往去前一个个比较,消灭他们,直到找到第一个符合条件(第一个比他大或者小)的元素,输出,然后他入栈。
单调队列:先看队头是不是不在窗口内了,然后每个新元素从队尾元素开始从后往前一个个比较,消灭他们,直到找到抵押给符合条件的元素,然后入队。输出队头元素

posted @ 2020-09-10 21:22  驿站Eventually  阅读(240)  评论(0编辑  收藏  举报