训练营走到第五十三天,今天进入的是一个一开始让我有点抗拒、但彻底搞清楚之后就特别顺手的内容:单调栈。三题连刷分别是 LeetCode 739. 每日温度、496. 下一个更大元素 I、503. 下一个更大元素 II,表面上是三道独立的题,实际操作下来你会发现它们是一条完整的“找下一个更大元素”能力升级线。如果你正在准备算法面试,或者刚刚开始接触栈这一类数据结构,我强烈建议把这一组题放在一起刷,因为从普通数组到子集查询再到循环数组,恰好把单调栈最核心的几个应用场景全过了一遍。
今天这篇不是简单的题解复述,我尽量把当时琢磨了很久的“为什么栈里要放下标”“为什么第二题不需要对 nums1 单独跑一遍模板”“环形数组为什么不复制也能解”这些问题都讲清楚,顺便把我踩过的坑也写出来。
1. 先搞清楚单调栈到底在解决哪一类问题
1.1 暴力解法的瓶颈:每个人都在往后“看”
LeetCode 739 的题目非常直观:给定一个每日温度数组,要求返回一个数组,每个位置表示“下一次出现比当天更高温度需要等几天”。我第一次看到这个题的反应是,这有什么难的,直接两层循环:
外层固定某一天 i,内层从 i + 1 开始往后找第一个比 temperature[i] 大的位置,找到就记下下标差。
暴力做法的时间复杂度是 O(n²),最坏情况可能是数组整体递减或者乱序但一直找不到更大值,内层循环几乎要扫到结尾。题目给的数组长度可能达到 10⁵,一旦出现 10⁵ 规模,O(n²) 就是约 100 亿次操作,在刷题平台上是铁定跑不过的。
很多人在这个阶段容易陷入一个误区:遇到数组题就习惯用“当前位置出发往后找”的思维。但我们要换一个角度——不是每个元素主动向后找,而是当遍历到某个较大的元素时,让它去“结算”前面所有等待答案的小元素。这正是单调栈的直觉。
1.2 一个打排球的类比:新来的人“唤醒”前面等待的人
我后来跟朋友讲单调栈,特别喜欢用排队看身高的例子。想象你有一排人,身高就是数值,现在问每个人“你右边第一个比你高的人是谁”。
如果从前往后逐个问,那就很慢,因为每个人都要回头扫描。单调栈的做法是,先维持一个队列,里面只放“还没找到答案的人”,并且让它们从队头到队尾保持身高递减。每次来一个新的人,如果新人的身高比队尾那个人高,说明队尾那人的答案出现了,可以让他出队;如果还比新的队尾高,就继续让新的队尾出队。直到新人的身高无法继续“碾压”队尾为止,再让新人入队。
这样的话,每个人最多入队一次、出队一次,总时间复杂度被压到 O(n)。那些还在队里没出去的人,说明右侧始终没有出现比自己更高的人,答案就是 0,对应题目里的“之后不会升温”。
这里的关键点在于:新元素出现的时候,不是只解决一个旧元素,而是可能一次性解决连续多个旧元素。所以代码里必须用 while 循环,不能只用 if。我第一次写的时候就顺手写了 if,结果数组里有连续比当前温度低的情况时全都漏了。
