Pattern 04 · Stack & Monotonic Stack

04 栈与单调栈 每个元素都在找「左/右第一个比我大的人」

面试频率 高题目 8 道模板 2 个⭐认脸 2 道
§1 Mental Model 一句话心智模型

先记住这一句

单调栈:每个元素都在找「左边或右边第一个比我大(或小)的人」;栈里存的,是那些还没等到答案的人。

新元素进场时,把栈里所有「等到答案了」的人依次弹出并结账——弹出的那一刻就是它的答案揭晓的那一刻。每个元素最多进出栈一次,所以整体 O(n),这就是它把 O(n²) 暴力扫描打下来的全部秘密。

普通栈是它的退化形态:不比大小、只配对嵌套结构(括号、路径、表达式),本章热身用它找感觉。

§2 Recognition Signals 识别信号

看到什么,想到本章

题面出现…想到
「下一个更大 / 更小元素」单调栈(模板 A)
「第一个比它高 / 低 / 便宜的…」单调栈(模板 A)
「柱状图 / 矩形 / 最大面积」单调栈(模板 A + 哨兵)
「滑动窗口的最大 / 最小值」单调队列(模板 B)
括号匹配、嵌套、「撤销上一步」普通栈
你写出的暴力解,内层循环在「向左/右扫,找第一个满足条件就停」单调栈优化信号
分流条件 · 决策树同源

「连续子数组 + 最值」且窗口大小固定 → 单调队列;窗口不固定、条件是「和 / 种类数」 → 滑动窗口(第 03 章);「子数组和恰好为 K」 → 前缀和 + 哈希(第 13 章)。

§3 Template 模板代码

可默写的那几行

■ 【骨架】 不变,闭着眼默写 ■ 【插槽】 随题变化,写题前先问自己填什么

模板 A · 下一个更大元素覆盖本章 80% 的题
def next_greater(nums):
    n = len(nums)
    ans = [-1] * n                          # 【插槽】默认值:没等到答案的人得什么
    stack = []                              # 【骨架】存下标;栈内对应值单调递减
    for i, x in enumerate(nums):            # 【骨架】
        while stack and nums[stack[-1]] < x:  # 【插槽】比较方向:< 找更大,> 找更小;带不带 = 见翻车点②
            j = stack.pop()                 # 【骨架】j 此刻等到了答案
            ans[j] = x                      # 【插槽】答案形式:值 x / 距离 i-j / 下标 i
        stack.append(i)                     # 【骨架】
    return ans
默写口诀:存下标、递减栈、弹出即结账。
模板 B · 单调队列仅 239 一题需要,认脸级
from collections import deque

def max_sliding_window(nums, k):
    q, ans = deque(), []                    # 【骨架】q 存下标,对应值单调递减,队首=窗口最大
    for i, x in enumerate(nums):
        while q and nums[q[-1]] <= x:       # 【骨架】新人把比自己弱的全部挤出去
            q.pop()
        q.append(i)
        if q[0] <= i - k:                   # 【插槽】过期条件:窗口左界怎么算
            q.popleft()
        if i >= k - 1:                      # 【插槽】何时开始产出答案
            ans.append(nums[q[0]])
    return ans
与模板 A 的唯一区别:窗口会从左边过期,所以要用双端队列在队首淘汰出窗的人。
§4 Problems 题目分层

从建直觉到扭模板

热身建直觉,easy 为主2 道
  • 纯配对:左括号进栈,右括号找栈顶结账。体会「栈顶 = 最近一个未完成的事」。
  • 辅助栈同步记录「当前最小」。插槽思维预演:栈里存什么由问题决定。
标准模板直接套3 道
变体模板要扭,坑的来源3 道
  • 先按位置排序算到达时间,再用栈合并车队。识别难度大于实现难度:它的「第一个追上我的人」结构藏在排序之后。
  • 模板 A 换方向(递增栈、找两侧第一个更矮的),末尾补哨兵 0 强制清算。见翻车点⑤,本章唯一必须掌握的 hard。
  • 模板 B 原题,认脸即可。
§5 Pitfalls & Self-check 常见翻车点 + 自测

会写不等于不翻车

  1. 存值还是存下标——默认存下标。答案涉及距离或位置(739)时值救不了你;存下标永远能取回值,反之不行。
  2. 严格与非严格(< vs <=)——有重复元素时决定相等的人算不算「答案」。84 题里用 >= 弹出可以简化等高柱的处理;写题前先问一句「两个相等的元素,谁给谁结账」。
  3. 方向记反——找「更大」用递减栈,找「更小」用递增栈。记法:栈里留着的都是「还没等到更大的人」,所以他们自然从底到顶递减。
  4. 循环数组忘了取模——503 走两遍时下标必须 i % n,第二遍只结账、不再入栈也可以(微优化,非必须)。
  5. 清算时机——遍历结束后栈里剩下的人是「永远没等到答案的人」。要么接受默认值(739 的 0),要么补哨兵强制结账(84 的末尾 0)——忘掉这一步是 84 题 WA 的头号原因。

自测 · 30 秒识别模式

30 秒识别Q1 · 901 Online Stock Span:返回今天之前连续多少天股价 ≤ 今天。你的第一反应是哪个模式?

看答案

单调栈。这是「之前第一个比我高的」的镜像问法:找到那个人,中间的天数就是答案,所以要存下标而不是值(翻车点①)。

30 秒识别Q2 · 402 Remove K Digits:删掉 k 个数字使剩下的数最小。你的第一反应是哪个模式?

看答案

单调栈。维持栈内递增,遇到更小的就把前面大的弹掉——「删掉一个大的」正是弹栈动作;贪心的直觉对,但落地结构是栈。

30 秒识别Q3 · 给定数组和整数 k,求「和恰好等于 k 的连续子数组」个数。你的第一反应是哪个模式?

看答案

前缀和 + 哈希,不是本章——「恰好为 K」是分流关键词,见 §2 分流条件。单调栈处理的是「比较大小」,不是「求和」。

每章自测都会混入恰好一道非本章题,防止「读哪章选哪章」的条件反射;题面也一律取自本章未收录的题,避免凭记忆而非凭信号作答。