单调栈-数据结构(C语言)

单调栈

顾名思义,单调栈就是栈内元素从栈顶到栈底单调递增或者单调递减的栈,这一点和单调队列很相似,但是单调栈只能在栈顶操作。

我们借用拿号排队的场景来说明。
现在有很多人在排队买可乐,每个人手里都拿着号,越靠前的人手里的号越小,但是号不一定是连续的。有一个人拿了号后并没有去排队,而是跑去约会了。等他回来后,发现队伍已经排得很长了,他不能直接插入到队伍里,不然人家以为他是来插队的。于是他跑到队伍最后,挨个询问排队人手里的号码是多少,他认为号比他大的人都是“插队”的,于是施魔法把这些人变消失,直到找到号比他小的为止。

在上面这个场景里,同学们排的队伍就像是单调栈,因为同学们手里拿的号是单调递增的。一个同学找到自己位置并加入队伍中的这个过程就是元素加入单调栈的过程。如果新加入的元素加到栈顶后,栈里的元素就不再是单调递增了,那么我们就删除加入前的栈顶元素,就像施魔法把“插队”的人变消失一样。只有当新元素加入后,栈依然是单调递增的,我们才把元素加进栈里。

单调栈例题

题目:给定一个包含若干整数的数组,对于其中每个元素 a r r i arr_i arri,计算左边离它最近的比 a r r i arr_i arri 更小的元素。
解法:给定一个包含若干个整数的数组,我们从第 1 个元素开始依次加入单调栈里,并且加入后更新单调栈。那么单调栈有这样的性质:对于从栈顶到栈底单调递减的栈,如果此时栈顶元素为b,加入新元素a后进行更新时,如果a大于b,说明如果从a在数组中的位置开始往左边遍历,则b一定是第一个比a小的元素;如果a小于b,那么对于a右侧的元素来说,b就失去了比较的意义,因此将b从栈中弹出,并继续让a和栈顶元素判断。

解法的伪代码如下:

get_left_smaller(arr, n)
    s = new Stack
    for element in arr
        while s is not empty and element < s.top
            s.pop
        if element > s.top
            s.push(element)

单调栈的维护是 m a t h c a l O ( n ) mathcal{O}(n) mathcalO(n) 级的时间复杂度,因为所有元素只会进出栈各一次。

一道单调栈的问题

我们来看看这样一道题:地上从左到右竖立着 n 块木板,从 1 到 n 依次编号,如下图所示。我们知道每块木板的高度,在第 n 块木板右侧竖立着一块高度无限大的木板,现对每块木板依次做如下的操作:对于第 i 块木板,我们从其右侧开始倒水,直到水的高度等于第 i 块木板的高度,倒入的水会淹没 a i a_i ai 块木板(如果木板左右两侧水的高度大于等于木板高度即视为木板被淹没)。求 n 次操作后,所有 a i a_i ai 的和是多少。
如图所示,在第 4 块木板右侧倒水,可以淹没第5 块和第6 块一共 2 块木板, a 4 a_4 a4 = 2。
在这里插入图片描述
现在你已经知道题意了,思考下,如果不考虑时间复杂度,该怎么写暴力程序呢?

暴力的时间复杂度是多少呢?在什么情况下可以用暴力程序呢?

如果要减小时间复杂度,该怎么优化暴力程序呢?有没有更高效的算法呢?

我们来分析下,什么时候水的高度会等于第 i 块木板的高度 h i h_i hi 呢,一定是水往右边漫延遇到了一块高度大于等于 h i h_i hi 的木板 j, a i a_i ai 就等于木板 i 和木板 j 之间的木板数。于是,问题就变成了寻找在第 i i i 个数右边第一个比它大的数。

我们可以暴力求解,从 1 循环到 n n n ,对每块木板再往右循环一遍,这样的时间复杂度是 O ( n 2 ) \mathcal{O}(n^2) O(n2) 的。有没有高效一点的做法呢?

我们回想下单调栈的性质,可以在某点左右扩展出一段连续区间,且该点在区间里始终保证是最值,和这题非常相似,而且这道题只要看点右侧扩展出来的区间即可。那么,接下来我们就用单调栈来解这道题。

单调栈解木板倒水问题

  1. 接下来我们解决单调栈解决木板倒水问题。
    首先我们在主函数里定义两个int 类型的 变量 n 和 ans , 且让ans 初始化为0。 n 表示有n 块木板,ans 用来记录最后的结果,接着让程序输出n.
  2. 接下来我们首先定义一个栈的之 指针 stack, 并分配一个Stack 类型的空间。接着调用初始化函数 init 完成初始化,参数长度设置为stack 和 n, 最后定义一个Node 类型的变量 temp , 用来存木板信息。
  3. 接下来我们要循环操作每个木板,维护一个栈底到栈顶单调递减的单调栈。具体算法如下:先输入第i 块的高度,然后标记下木板的编号,记录到变量 temp 里, 接着,temp 依次 和栈顶元素a 比较,如果a 的高度小于 等于 temp 的高度,则弹出。
    根据单调栈的性质,元素a 出栈表明我们已经找到元素a 右侧第一个比他大元素了(这里指得是temp), 元素temp 和 元素a 之间隔的元素个数等于 temp 的编号减去a 的编号 再减1,累加结束后我们进行新的比较,重复上述操作,直到栈顶元素的高度大于 temp 的高度,然后我们再把 temp 加入栈里。
    循环结束后,我们还需要判断栈是否为空,如果不为空,则依次弹出栈顶元素,操作和上面的一样. 最后输出结果,算法结束。
    了解了算法,我们先把循环写好,用变量i 从1 循环到不小于等于n 时退出,只是写循环框架即可,稍后我们再来写全。
  4. 我们先让程序输入木板的高度,记录在temp 的height 里,然后让 temp 的id 等于 变量i, 表示这是第 i 块木板。
  5. 输入第i 块木板,接下来我们要看看栈顶木板的高度是不是小于等于当前木板的高度,如果是则删除,直到不满足条件为止。
    这里我们用while 循环来实现,在此之气那还需要满足另一个条件;栈不能为空,否则取栈顶元素和删除栈顶都是非法操作了。 这一步只要把while 循环写好,循环内部我们后面再来实现。
  6. 按之前讨论的算法,如果有元素出栈则表明我们已经找到了木板p 右侧第一比他高的木板q, 此时的木板p 是栈顶元素,木板q 就是当前元素,两块木板之间隔得木板数 a i a_i ai i d q − i d p − 1 id_q - id_p - 1 idqidp1.
    我们先把 a i a_i ai 累加到ans 里, 加完后,我们再把栈顶元素给删除了.
  7. 栈顶木板高度现在已经是大于当前木板高度了 ,当然课可能此时栈为空了,接下来我们调用push 函数 把当前 得木板 temp 插入到栈里。
  8. 这样我们把这个 for 循环 也写完了,当循环一遍后,我们要考虑下此时栈是否为空, 如果栈不为空则要挨个弹出。
  9. 我们先把while 循环写好,条件即栈不为空。
    这里弹出栈顶元素时,和for 循环里的操作一样,计算 a i a_i ai 的值,然后加到ans 里,不同的时这时的位置应该为 n + 1, 然后弹出栈顶元素。
    10 .到这里我们所有的计算都已经完成了, 现在再while 循环后面把ans 输出即可,在结果后面记得加个换行。
    最后,我们还要调用clear 函数把栈stack 占用的内存释放掉,避免发生内存泄露。
#include <stdio.h>
#include <stdlib.h>

#define ERROR 0
#define OK 1

typedef struct Node {
    int id, height;
} Node;

typedef struct Stack {
    Node *elements;
    int max_size, top_index;
    
} Stack;

void init(Stack *s, int length) {
    s->elements = (Node *)malloc(sizeof(Node) * length);
    s->max_size = length;
    s->top_index = -1;
}

int push(Stack *s, Node element) {
    if (s->top_index >= s->max_size - 1) {
        return ERROR;
    }
    s->top_index++;
    s->elements[s->top_index] = element;
    return OK;
}

int pop(Stack *s) {
    if (s->top_index < 0) {
        return ERROR;
    }
    s->top_index--;
    return OK;
}

Node top(Stack *s) {
    return s->elements[s->top_index];
}

int empty(Stack *s) {
    if (s->top_index < 0) {
        return 1;
    } else {
        return 0;
    }
}

void clear(Stack *s) {
    free(s->elements);
    free(s);
}

int main() {
    int n, ans = 0;
    scanf("%d",&n);
    Stack *stack = (Stack *)malloc(sizeof(Stack));
    init(stack, n);
    Node temp;
    for (int i =1 ; i <= n; i++) {
        scanf("%d",&temp.height);
        temp.id = i;
        
        while(!empty(stack) && top(stack).height <= temp.height) {
            ans = ans + i  - top(stack).id - 1;
            pop(stack);
        }
        push(stack, temp);
       
    }
     while(!empty(stack)){
          ans = ans + n + 1 - top(stack).id  - 1;
          pop(stack);
    
     }
    printf("%d\n",ans);
    clear(stack);
    return 0;
}
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值