AI summary
type
status
date
slug
summary
category
tags
icon
password
栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。
听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一类典型的问题,比如「下一个更大元素」,「上一个更小元素」等。本文用讲解单调队列的算法模版解决「下一个更大元素」相关问题,并且探讨处理「循环数组」的策略。至于其他的变体和经典例题,我会在下一篇文章 单调栈变体和经典习题 讲解。
单调栈模版
现在给你出这么一道题:输入一个数组
nums,请你返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。比如说,输入一个数组
nums = [2,1,2,4,3],你返回数组 [4,2,4,-1,-1]。因为第一个 2 后面比 2 大的数是 4; 1 后面比 1 大的数是 2;第二个 2 后面比 2 大的数是 4; 4 后面没有比 4 大的数,填 -1;3 后面没有比 3 大的数,填 -1。这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是 。
这个问题可以这样抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的下一个更大元素呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的下一个更大元素,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。

这个情景很好理解吧?带着这个抽象的情景,先来看下代码。
这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个「个子高」元素之间的元素排除,因为他们的存在没有意义,前面挡着个「更高」的元素,所以他们不可能被作为后续进来的元素的下一个更大元素了。
分析它的时间复杂度,要从整体来看:总共有
n 个元素,每个元素都被 push 入栈了一次,而最多会被 pop 一次,没有任何冗余操作。所以复杂度为 。问题变形
单调栈的代码实现比较简单,下面来看一些具体题目。
496. 下一个更大元素 I
nums1中数字x的 下一个更大元素 是指x在nums2中对应位置 右侧 的 第一个 比x大的元素。给你两个 没有重复元素 的数组nums1和nums2,下标从 0 开始计数,其中nums1是nums2的子集。对于每个0 <= i < nums1.length,找出满足nums1[i] == nums2[j]的下标j,并且在nums2确定nums2[j]的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是-1。返回一个长度为nums1.length的数组ans作为答案,满足ans[i]是如上所述的 下一个更大元素 。提示:
1 <= nums1.length <= nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 10^4
nums1和nums2中所有整数 互不相同
nums1中的所有整数同样出现在nums2中进阶:你可以设计一个时间复杂度为O(nums1.length + nums2.length)的解决方案吗?
这道题给你输入两个数组
nums1 和 nums2,让你求 nums1 中的元素在 nums2 中的下一个更大元素。其实直接用上面的函数模版就可以解决这道题了,因为题目说 nums1 是 nums2 的子集,那么我们先把 nums2 中每个元素的下一个更大元素算出来存到一个映射里,然后再让 nums1 中的元素去查表即可:739. 每日温度
给定一个整数数组temperatures,表示每天的温度,返回一个数组answer,其中answer[i]是指对于第i天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用0来代替。
这个问题本质上也是找下一个更大元素,只不过现在不是问你下一个更大元素的值是多少,而是问你当前元素距离下一个更大元素的索引距离而已。
相同的思路,直接调用单调栈的算法模板,稍作改动就可以,直接上代码吧:
单调栈讲解完毕,下面开始另一个重点:如何处理「循环数组」。
如何处理环形数组
503. 下一个更大元素 II
给定一个循环数组nums(nums[nums.length - 1]的下一个元素是nums[0]),返回nums中每个元素的 下一个更大元素 。数字x的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出-1。
比如输入
[2,1,2,4,3],你应该返回 [4,2,4,-1,4],因为拥有了环形属性,最后一个元素 3 绕了一圈后找到了比自己大的元素 4。这个问题肯定还是要用单调栈的解题模板,但难点在于,比如输入是
[2,1,2,4,3],对于最后一个元素 3,如何找到元素 4 作为下一个更大元素。对于这种需求,常用套路就是将数组长度翻倍:

这样,元素 3 就可以找到元素 4 作为下一个更大元素了,而且其他的元素都可以被正确地计算。
有了思路,最简单的实现方式当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,我们可以不用构造新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果。直接看代码吧:
这样,就可以巧妙解决环形数组的问题,时间复杂度 O(N)。
最后提出一些问题吧,本文提供的单调栈模板是
nextGreaterElement 函数,可以计算每个元素的下一个更大元素,但如果题目让你计算上一个更大元素,或者计算上一个更大或相等的元素,应该如何修改对应的模板呢?而且在实际应用中,题目不会直接让你计算下一个(上一个)更大(小)的元素,你如何把问题转化成单调栈相关的问题呢?敬请期待下一篇!