【单调栈】总结

单调栈有什么用?

栈为容器,特性是后入先出。

经典栈的应用场景大概为:浏览器的后退按钮实现等。即:栈的一个应用场景就是状态保持。

单调栈和经典栈的区别是,栈是一股脑的存,单调栈是让栈内的元素(或者是栈内元素的对应元素)具有单调的特性。

那这个单调的特性有啥用呢?我们不考虑其他的,只考虑栈内元素和待入栈元素。

举个例子:栈内元素如果是单调递减的,[5,4,2,1]。如果我们待入栈元素是3,为了维持单调栈,我们应该让1先出栈,然后2再出栈(因为他们比3小)。然后碰到4就不能出栈了,让3入栈。

在这个处理过程中,我们就得到了下一个比3大的元素【4】。

我们更深入一些,我们不考虑如何生成单调栈。 对于某个序列来讲 [3,1,2,4,5],我们就直接告诉你,我们处理到3的时候,生成的单调栈是[5,4,2,1],待入栈元素是3. 那么这么做有什么好处呢,换句话说,我用遍历不可以吗,我为什么要用单调栈呢? 如果我要找比3大的第一个元素,我直接从3往后遍历,找到4就结束了;如果我找比1大的第一个元素,我就往后遍历,找到2就结束了;如果我想找比2大的第1个元素,我们往后找1个,找到4就结束了;如果我们想找比4大的第1个元素,我们往后找1个,找到5就结束了。 

这样实现真是短平快,又好想,很无脑。但我们这样做真的合理吗?举个极端点的例子吧。如果某个序列是[5,4,3,2,1],我们要找比每个元素大的下一个元素,对于每个元素来说,我们都要遍历到末尾才能发现压根就没有任何元素比它大。而我们用单调栈,就可以在O(n)的时间内搞定。

我们从后往前遍历(先不说为什么,先看)生成单调栈。当遍历到1时,单调栈为空,代表已经遍历过(其实没有遍历过,但为了归一化处理,我们可以认为已经遍历过)的元素中没有比1更大的元素,我们生成的单调栈为[1];遍历到2时,我们的1弹栈了(因为如果直接让2入栈,就不符合单调递减了,下同),单调栈空了,代表已经遍历过的元素中没有比2更大的元素,我们生成的单调栈为[2];遍历到3时,我们的2弹栈了,单调栈空了,代表已经遍历过的元素中没有比3更大的元素,我们生成的单调栈为[3];。。。。。。

看明白了吗,使用暴力,我们是O(n**2)的时间复杂度和O(1)的空间复杂度;使用单调栈,我们是O(n)的时间复杂度和O(1)的空间复杂度。

那么,我们凭啥能用单调栈以空间换时间呢?

还记得吗,我们说了,栈的一个应用场景就是状态保持。但,如果只用状态保持,我们只用栈不就好了吗?用什么单调栈呢。

说干就干,我们用栈试试。

如果某个序列是[5,4,3,2,1],我们从后往前遍历的话,遍历到1时,栈为空, 代表没有比1更大的值,1入栈;遍历到2时,栈不为空,我们先把栈里的东西全倒出来看看(当然,这里就是1),1没2大,所以再按顺序把它倒回去(别忘了它是栈)。。。。。

等等,好像有哪里不对。既然是这样,我何必用栈呢,我用set()存不行吗,我用列表存不行吗,我用dict存不行吗?

问对了。何必用栈呢。只用状态保持的话,我们完全没必要用栈。

还记得吗,单调栈是让栈内的元素是单调的。

再举个例子:

[5,9,4,3,1,7]

我们生成单调栈。[7],[7,1],[7,3],[7,4],[9],[9,5]

看这个单调栈,我们赋予其含义:

当生成为[7]时,我们保证在7之后,>=7的就是7.

当生成为[7, 1]时,我们保证在1之后,>=1的是1,再大的就是7

当生成为[7,3]时,我们保证在3之后,>=3的是3,再大的就是7。

 

单调栈更像是一种状态,这个状态保持了我们已经遍历过的元素的排序状态,因为它是单调的,所以它是有序的;因为它是个栈,所以它在下标上是绝对有序的。

即,单调栈做到了在下标有序(下一个)的基础上做到元素有序(更大的元素)。

posted @ 2023-10-09 13:00  BJFU-VTH  阅读(19)  评论(0编辑  收藏  举报