单调栈 / 单调队列专题:Mermaid 图解维护顺序、窗口最值与 4 道 LeetCode 核心题
用 Mermaid 图解单调栈与单调队列的维护逻辑、最近更大更小元素、滑动窗口最值和 4 道 LeetCode 经典题,系统掌握单调结构专题。
单调栈 / 单调队列专题:Mermaid 图解维护顺序、窗口最值与 4 道 LeetCode 核心题
单调栈和单调队列属于那种“第一次看很绕,一旦想通就特别稳定”的题型。
它们真正难的地方,不在代码,而在于你要先想明白:
- 栈或队列里到底维护什么
- 为什么某些元素可以被提前弹掉
- 什么叫“最近更大 / 更小”
- 滑动窗口为什么要维护下标而不是值
这篇文章就把这些核心画成 Mermaid 图,再用 4 道 LeetCode 题把单调栈和单调队列的典型模型串起来。
学习目标:
- 理解单调栈和单调队列的维护逻辑。
- 掌握“最近更大 / 更小元素”模型。
- 理解滑动窗口最值为什么适合单调队列。
- 用 4 道 LeetCode 题覆盖单调结构高频模型。
- 用一张知识卡片形成单调结构题的判断框架。
一、单调结构的本质:维护“有用候选人”
无论是单调栈还是单调队列,本质都是:
把未来不可能再有用的元素提前淘汰掉,只保留可能成为答案的候选人。
flowchart TD
A[新元素进入] --> B[和栈顶/队尾比较]
B --> C{旧元素还有用吗}
C -->|没有| D[弹出旧元素]
C -->|有| E[保留]
D --> F[继续比较]
E --> G[把新元素加入结构]
这就是“单调”两个字的真正意义:
- 栈或队列中的元素按照某种顺序保持单调
- 一旦新元素更优,旧元素就可以淘汰
二、单调栈:处理最近更大 / 更小元素
单调栈最常见的用途是:
- 下一个更大元素
- 下一个更小元素
- 左边第一个更大 / 更小元素
- 柱状图面积
以“下一个更大元素”为例
维护一个递减栈:
- 栈内从栈底到栈顶递减
- 当新元素更大时,说明它就是栈顶元素的“下一个更大值”
flowchart TD
A[当前元素 x] --> B{栈顶元素 < x ?}
B -->|是| C[弹出栈顶]
C --> D[当前 x 就是它的下一个更大元素]
D --> B
B -->|否| E[x 入栈]
三、单调队列:处理滑动窗口最值
单调队列最经典的用途是滑动窗口最大值 / 最小值。
核心思想:
- 队列里存的是“可能成为窗口最值”的下标
- 队头永远是当前窗口答案
flowchart TD
A[新元素下标 i 进入窗口] --> B[从队尾弹出比它更差的元素]
B --> C[i 入队]
C --> D[检查队头是否已滑出窗口]
D --> E[队头就是当前窗口最值]
为什么通常存下标而不是值?
因为窗口会移动,你必须知道某个元素有没有过期。
四、为什么“旧元素可以放心弹掉”
这一步是理解单调结构的关键。
单调栈
如果当前元素已经比栈顶更优,那么栈顶以后再也不可能成为当前方向上的答案。
单调队列
如果一个更大的新元素进入窗口,那么队尾更小的旧元素只会更早过期、值还更差,因此没有保留意义。
flowchart LR
A[旧元素 更差] --> C[可以弹出]
B[新元素 更优 且更靠后] --> C
五、4 道 LeetCode 题目打通单调结构专题
1)LeetCode 496. 下一个更大元素 I
题型定位:单调栈基础题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, int> next_greater;
vector<int> st;
for (int num : nums2) {
while (!st.empty() && st.back() < num) {
next_greater[st.back()] = num;
st.pop_back();
}
st.push_back(num);
}
vector<int> res;
for (int num : nums1) {
res.push_back(next_greater.count(num) ? next_greater[num] : -1);
}
return res;
}
};
flowchart TD
A[遍历 nums2] --> B{当前值 > 栈顶值}
B -->|是| C[弹出栈顶并记录答案]
C --> B
B -->|否| D[当前值入栈]
这题练的是:
- 单调栈维护逻辑
- 为什么弹出时可以立即确定答案
2)LeetCode 739. 每日温度
题型定位:单调栈 + 下标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = static_cast<int>(temperatures.size());
vector<int> res(n, 0);
vector<int> st;
for (int i = 0; i < n; ++i) {
while (!st.empty() && temperatures[st.back()] < temperatures[i]) {
int idx = st.back();
st.pop_back();
res[idx] = i - idx;
}
st.push_back(i);
}
return res;
}
};
flowchart TD
A[当前天 i] --> B{当前温度 > 栈顶下标对应温度}
B -->|是| C[弹出 idx]
C --> D[答案 = i - idx]
D --> B
B -->|否| E[i 入栈]
这题训练的是:
- 为什么栈里要放下标
- 距离类问题如何由下标差得到答案
3)LeetCode 84. 柱状图中最大的矩形
题型定位:单调栈进阶题。
核心:当某根柱子出栈时,说明它左右两侧第一个更小元素已经确定,于是可以计算以它为高的最大矩形面积。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> h(heights.size() + 2, 0);
copy(heights.begin(), heights.end(), h.begin() + 1);
vector<int> st;
int ans = 0;
for (int i = 0; i < static_cast<int>(h.size()); ++i) {
while (!st.empty() && h[st.back()] > h[i]) {
int height = h[st.back()];
st.pop_back();
int left = st.back();
int width = i - left - 1;
ans = max(ans, height * width);
}
st.push_back(i);
}
return ans;
}
};
flowchart TD
A[当前高度更小] --> B[栈顶柱子出栈]
B --> C[它的右边界 = 当前下标左一位]
C --> D[它的左边界 = 新栈顶右一位]
D --> E[计算面积]
这题训练的是:
- 单调栈不只是找下一个更大元素
- 出栈时机就是答案确定时机
4)LeetCode 239. 滑动窗口最大值
题型定位:单调队列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq;
vector<int> res;
for (int i = 0; i < static_cast<int>(nums.size()); ++i) {
while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back();
dq.push_back(i);
if (dq.front() <= i - k) dq.pop_front();
if (i >= k - 1) res.push_back(nums[dq.front()]);
}
return res;
}
};
flowchart TD
A[新下标进入] --> B[从队尾弹出更小值下标]
B --> C[新下标入队]
C --> D[若队头下标过期则弹出]
D --> E[队头对应值就是窗口最大值]
这题训练的是:
- 单调队列维护窗口最大值
- 为什么要同时处理“更差元素淘汰”和“过期元素淘汰”
六、单调栈和单调队列怎么快速判断
flowchart TD
A[看到题目] --> B{是否在找最近更大 / 更小元素}
B -->|是| C[单调栈]
B -->|否| D{是否是滑动窗口最值}
D -->|是| E[单调队列]
D -->|否| F{是否需要在出栈时结算答案}
F -->|是| G[单调栈进阶题]
F -->|否| H[再看是否适合其他模型]
七、单调结构常见错误
1)分不清维护递增还是递减
这取决于你要找“更大”还是“更小”。
2)该存下标却只存值
涉及距离或窗口过期时,通常必须存下标。
3)弹出条件写反
单调结构的核心就在比较条件,一旦反了,整题都会错。
4)窗口过期条件写错
滑动窗口题必须判断队头下标是否已经离开窗口。
flowchart TD
A[窗口右移] --> B[队头下标是否过期]
B -->|是| C[弹出队头]
B -->|否| D[保留]
八、单调结构知识卡片
mindmap
root((单调结构))
单调栈
最近更大
最近更小
出栈结算
单调队列
滑动窗口最大值
滑动窗口最小值
队头是答案
关键点
维护有用候选人
弹出无用元素
常存下标
常错点
比较方向错
过期条件错
值和下标混用
复习版要点:
- 单调结构的本质是“保留有用候选人”
- 最近更大 / 更小元素,优先想到单调栈
- 滑动窗口最值,优先想到单调队列
- 距离和过期问题通常都要存下标
- 弹出条件和维护顺序是整题核心
九、最后总结
如果只记一句话,请记这个:
单调栈和单调队列,不是在机械维护顺序,而是在主动淘汰未来不可能有用的元素。
做题时先判断:
- 我要找的是最近更大 / 更小,还是窗口最值
- 结构里该存值还是存下标
- 元素什么时候应该被弹掉
把这篇里的 4 道题做透,单调结构题的主干套路就基本建立起来了。
本文由作者按照 CC BY 4.0 进行授权