30 秒识别Q1 · 901 Online Stock Span:返回今天之前连续多少天股价 ≤ 今天。你的第一反应是哪个模式?
看答案
单调栈。这是「之前第一个比我高的」的镜像问法:找到那个人,中间的天数就是答案,所以要存下标而不是值(翻车点①)。
新元素进场时,把栈里所有「等到答案了」的人依次弹出并结账——弹出的那一刻就是它的答案揭晓的那一刻。每个元素最多进出栈一次,所以整体 O(n),这就是它把 O(n²) 暴力扫描打下来的全部秘密。
普通栈是它的退化形态:不比大小、只配对嵌套结构(括号、路径、表达式),本章热身用它找感觉。
| 题面出现… | 想到 |
|---|---|
| 「下一个更大 / 更小元素」 | 单调栈(模板 A) |
| 「第一个比它高 / 低 / 便宜的…」 | 单调栈(模板 A) |
| 「柱状图 / 矩形 / 最大面积」 | 单调栈(模板 A + 哨兵) |
| 「滑动窗口的最大 / 最小值」 | 单调队列(模板 B) |
| 括号匹配、嵌套、「撤销上一步」 | 普通栈 |
| 你写出的暴力解,内层循环在「向左/右扫,找第一个满足条件就停」 | 单调栈优化信号 |
「连续子数组 + 最值」且窗口大小固定 → 单调队列;窗口不固定、条件是「和 / 种类数」 → 滑动窗口(第 03 章);「子数组和恰好为 K」 → 前缀和 + 哈希(第 13 章)。
■ 【骨架】 不变,闭着眼默写 ■ 【插槽】 随题变化,写题前先问自己填什么
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
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
ans[j] = i - j(答案要距离,这就是为什么骨架存下标)。2n 次、下标取 i % n,其余不变。< vs <=)——有重复元素时决定相等的人算不算「答案」。84 题里用 >= 弹出可以简化等高柱的处理;写题前先问一句「两个相等的元素,谁给谁结账」。i % n,第二遍只结账、不再入栈也可以(微优化,非必须)。30 秒识别Q1 · 901 Online Stock Span:返回今天之前连续多少天股价 ≤ 今天。你的第一反应是哪个模式?
单调栈。这是「之前第一个比我高的」的镜像问法:找到那个人,中间的天数就是答案,所以要存下标而不是值(翻车点①)。
30 秒识别Q2 · 402 Remove K Digits:删掉 k 个数字使剩下的数最小。你的第一反应是哪个模式?
单调栈。维持栈内递增,遇到更小的就把前面大的弹掉——「删掉一个大的」正是弹栈动作;贪心的直觉对,但落地结构是栈。
30 秒识别Q3 · 给定数组和整数 k,求「和恰好等于 k 的连续子数组」个数。你的第一反应是哪个模式?
前缀和 + 哈希,不是本章——「恰好为 K」是分流关键词,见 §2 分流条件。单调栈处理的是「比较大小」,不是「求和」。
每章自测都会混入恰好一道非本章题,防止「读哪章选哪章」的条件反射;题面也一律取自本章未收录的题,避免凭记忆而非凭信号作答。