【单调栈】不是,你怎么想出来的单调栈?

一张小图

学习某一个算法,不要总是去背诵这个东西怎么用,用的时候模版是什么,我要在什么地方套用它,这样我认为是学不会的。以下是我对单调栈的个人思考,并且对发明这种方法的人感到由衷的赞叹和佩服。至少,我是思考不到的。

场景

力扣739.每日温度
假如你和我一样不喜欢高温,那可能和我一样喜欢看天气预报,总是看看今天温度多高,还有没有比今天更高的温度。现在有一个温度的列表,就假如说是[73,74,75,71,69,72,76,73],我想知道对于上面每一天,下一个温度更高的地方在之后几天出现?

直观一点的话怎么看

很直观的想法就是——遍历一下后面看看嘛:

  • 73的后面是74,找到了,是1
  • 去看74,后面是75,是1
  • 再看75,71、69、72跟75比较,都不是,直到找到76,与75比较,大于75,所以答案在后面第4天

没问题吧?一点问题都没有,这个问题很好地解决掉了。

总有人想着优化

人和人写的代码都要被优化…Oh no,不要优化我……
那就想想吧,上面的代码时间复杂度几乎是 O ( n 2 ) O(n²) O(n2)。[35,34,33,32,31],如果是这种,那就是妥妥的平方级别。对于这道题,向后遍历是一定的,那就要考虑,遍历的方式是不是不够优秀?

看一下75那个地方的遍历,按照上面暴力算法,接下来的动作是这样的:

  • 遍历了71、69、72,找到了75的下一个更高的温度是76,记录到结果中;
  • 然后看71,71再次看69、72,好的,71的结果是2,69同理;
  • 接着去看72,再次看76,找到72的结果是1

如果我能省去再看的过程,就可以减轻大量的时间复杂度,而困难的地方就在于,我们要怎么省掉它。

单调栈

以上的暴力算法就是一遍遍的遍历。只是,遍历过程中的数字我们没有考虑重复利用的可能性,这是造成时间浪费的一部分原因。有时优化就是对中间结果或重要的中间量以某种形式保存,下一次根据这个值的意义直接以例如 O ( 1 ) O(1) O(1)的复杂度直接读取,来降低时间复杂度。 比如说动态规划就是这种思路,算过的值下次就不要再搜索一遍了。接下来我们就考虑,中间的值有什么价值。
首先,73、74这两个数只能这样计算了,我遍历到74,实际上就已经知道73的结果了,到此,73这个数已经没有作用了。应该没有意见。那么74同样的道理。
然后再次考虑计算75的过程,这里才是重头戏。我们要的值在76的地方。中间会经过71、69、72。如果我们想优化,也是在这几个地方下功夫,毕竟前几个数的耗时还是 O ( 1 ) O(1) O(1)
盯着下面的数字,你是否有什么想法?
在这里插入图片描述
-------- 我是分割线 ---------

一些发现

如果你敏锐地注意到,过程中遍历的72虽然不是75的答案,但是已经可以为中间的71、69找到答案时,你对找到优化的方法已经很近了,这确实需要一些思考。
这时候我可能就要重新考虑每个值的意义了。通过重新思考每个值还能有什么意义,或许可以找到突破口。接下来可能有点跳跃,需要一点灵光乍现。当你遍历到72时,69是第一个没找到答案的温度,71是第二个,75是第三个。想到69是第一个没找到答案的值这一点很重要,因为接下来每一次赋值,都是给第一个没找到答案的值赋值。 之前的74和73,依然没有任何作用。而72,正是为第一个没有找到答案的温度找到了答案。有点想法了吗?我们可以继续沿着这条线想下去。 在为69找到答案时,69还有没有作用?已经没有了,左边不会有数字是以它作为答案,因为你已经遍历到了它的右边,已经证明它不是别人的答案。然后,71变为第一个没有找到答案的数字。 可能已经蒙了,我们简单总结一下,大家可以一条一条过,看看是否都认可:

  • 为一个值(如75)寻找答案时,中间遍历过的值如果有多个,其中可能就有一部分可以找到答案,如72完全可以将71、69的答案找到,后面不用再为71、69遍历。
  • 既然72可以为69和71找到答案,当这么想的时候,我们就不再是单单为每一个值遍历后面的数组。即为遍历到的值的前面的值找答案。
  • 在遍历到72时,考虑前面的69是第一个没找到答案的值,71是第二个,依次类推。
  • 72可以为第一个没找到答案的值找到答案。
  • 71变成第一个没找到答案的值,72还可以为它找到答案。
  • 75变成第一个没找到答案的值,72不符合条件。这时就必须继续遍历了。

数据结构

这个思考过程理解了的话,应该自然而然或者并不是很自然地想到栈了。毕竟很明显的后进先出,我们遍历过的元素,不符合条件的就存储起来,遇到可以找到元素的值就弹出。
至于为什么叫单调栈呢?原因是,栈内的值是 [ 75 , 71 , 69 [75,71,69 [75,71,69,从栈顶到栈底是单调的,细想一下遍历的过程,一直在找比当前的值大的元素,如果不是则存到栈内,因此它的名字是单调栈。

时间复杂度

直接降为 O ( n ) O(n) O(n),其实最多遍历两次,一次向后搜索,一次根据栈内存储的值为结果集赋值。

单调栈

单调栈的作用,就是存储遍历过的值,通常用于去求按一个方向找到第一个比它大或比它小的值。仔细体会上面的过程,应该能够理解。实现的代码也很简单,就是简单地模拟上面的过程去压栈和弹栈。

每日温度的题解

为了很方便地找到元素,我们在单调栈中通常存入下标,再通过下标去比较当前的值。如果没有接触题目的可以先去简单读一下题力扣739.每日温度,再来尝试理解存储数组下标的妙用。当然,不同的场景可以灵活的应用,对于本文,了解了为什么使用栈即可。

每一次遍历时都去与栈顶的值比较,即是否能为第一个没找到答案的值找到答案

class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int[] ret = new int[temperatures.length];
        if (temperatures.length <= 1) return ret;
        // 单调栈,存放已经遍历的元素
        Deque<Integer> stack = new LinkedList<Integer>();
        stack.push(0);
        for (int i = 1; i < temperatures.length; i++) {
            while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
                ret[stack.peek()] = i - stack.pop();
            }
            
            stack.push(i);
        }
        return ret;
    }
}
  • 33
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不愿做小白阿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值