别再被“下一个更大元素”吓到!一文搞懂单调栈,从此刷题如喝水!
🎯 你是否也曾被这些题目折磨过?
- “给定每天的气温,告诉我哪天会更热?”(739. 每日温度)
- “在一个循环数组里,每个数的下一个更大值是啥?”(503. 下一个更大元素 II)
- “柱状图里能接多少雨水?”(42. 接雨水)
- “柱状图中最大的矩形面积是多少?”(84. 柱状图中最大的矩形)
是不是一看就头大?别慌!这些看似八竿子打不着的问题,其实都藏着同一个“幕后黑手”——单调栈(Monotonic Stack) !
今天,我们就来揭开它的神秘面纱,让你笑着把这几道高频面试题拿下!
🧠 什么是单调栈?它到底“单调”在哪?
单调栈 = 一个只允许特定顺序进出的栈。
想象你在排队买奶茶,店员说:“只允许身高递减的人进队!”
于是你发现:队伍里的人越来越矮(或相等),一旦有人比前面高,就得先把前面“不够格”的人请出去。
这就是单调栈的核心思想:
- 维护一个“单调递减”或“单调递增”的索引序列
- 当新元素破坏了这个顺序,就弹出旧元素,并趁机计算答案
💡 重点:栈里存的是索引(index),不是值! 这样才能算距离、宽度、天数……
🚀 四大经典题型,一套模板通杀!
下面这四类问题,本质都是“找右边第一个比当前大的元素”,只是问法不同:
| 题目 | 问法 | 返回内容 |
|---|---|---|
| 739. 每日温度 | “几天后更热?” | 天数差(i - j) |
| 496. 下一个更大元素 I | “nums1 中每个数在 nums2 右边第一个更大的数?” | 具体数值 |
| 503. 下一个更大元素 II | “循环数组中的下一个更大值?” | 循环遍历 + 数值 |
| 84 & 42 | “能围成多大面积?” | 宽度 × 高度 |
虽然表面不同,但内核一致:用单调栈找到“边界”!
🔥 模板来了!通用单调栈写法(递减栈)
const stack = [];
for (let i = 0; i < arr.length; i++) {
// 当前元素比栈顶大 → 破坏了递减性
while (stack.length > 0 && arr[i] > arr[stack[stack.length - 1]]) {
const topIndex = stack.pop();
// ✅ 此时可以计算 topIndex 的答案!
// 比如:result[topIndex] = i - topIndex;
// 或:area = height[topIndex] * (i - stack[栈顶] - 1)
}
stack.push(i); // 把当前索引入栈
}
🎯 关键洞察:
弹出topIndex时,左边第一个比它小的是stack[新栈顶],右边第一个比它小的是i。
这就是“接雨水”和“最大矩形”的宽度来源!
🌧️ 实战演练:从“找温度”到“接雨水”
✅ 1. [739. 每日温度] —— 找“更热的那天”
var dailyTemperatures = function(temps) {
const res = new Array(temps.length).fill(0);
const stack = [];
for (let i = 0; i < temps.length; i++) {
while (stack.length && temps[i] > temps[stack.at(-1)]) {
const j = stack.pop();
res[j] = i - j; // 天数差!
}
stack.push(i);
}
return res;
};
💬 心理活动:
“73度?明天74,那我今天就知道1天后更热!”
“76度?后面没人比我高了,那就填0,认命吧。”
✅ 2. [42. 接雨水] —— 利用“凹槽”积水
核心思想:雨水 = 左右两边的“墙”夹住中间的“坑”
var trap = function(height) {
let res = 0;
const stack = [];
for (let i = 0; i < height.length; i++) {
while (stack.length && height[i] > height[stack.at(-1)]) {
const mid = stack.pop();
if (!stack.length) break;
const left = stack.at(-1);
const h = Math.min(height[left], height[i]) - height[mid];
const w = i - left - 1;
res += h * w;
}
stack.push(i);
}
return res;
};
💬 想象:
栈里存的是“可能形成左墙”的位置。
当遇到更高的右墙,就赶紧算中间能接多少水!
✅ 3. [84. 最大矩形] —— 每个柱子当“最低点”
技巧:在数组前后加0,确保所有柱子都会被弹出计算!
heights = [0, ...heights, 0]; // 哨兵
for (let i = 0; i < heights.length; i++) {
while (stack.length && heights[i] < heights[stack.at(-1)]) {
const mid = stack.pop();
const h = heights[mid];
const w = i - stack.at(-1) - 1;
res = Math.max(res, h * w);
}
stack.push(i);
}
💡 为什么加0?
不加的话,最后栈里一堆没处理的柱子,还得额外清空。加了哨兵,自动收尾!
✅ 4. [503. 循环数组] —— 遍历两次!
把数组“复制一遍”(用 i % n 模拟),就能处理循环!
for (let i = 0; i < 2 * n; i++) {
const idx = i % n;
while (stack.length && nums[idx] > nums[stack.at(-1)]) {
const j = stack.pop();
result[j] = nums[idx];
}
stack.push(idx);
}
🔄 循环的本质:让每个元素都有机会看到“后面的+开头的”
🤔 为什么单调栈这么强?
因为它高效地记录了“潜在答案的候选者” 。
- 暴力解法:对每个元素向右扫描 → O(n²)
- 单调栈:每个元素入栈出栈一次 → O(n)
而且,它天然适合处理“最近的更大/更小值”这类问题。
🎁 小贴士:如何判断该用单调栈?
当你看到以下关键词,立刻警觉:
- “下一个更大/更小元素”
- “右侧第一个比它大的数”
- “能接到多少雨水”
- “最大矩形面积”
- “柱状图”、“高度”、“宽度”
✅ 记住口诀: “找边界,算距离,用栈存索引!”
🌈 结语:从恐惧到掌控
曾经,我也看着“接雨水”发呆,觉得这是天才才能想出来的解法。
后来才发现,单调栈不过是一个优雅的“排队规则” 。
它不玄学,不魔法,只是用栈维护了一种秩序,让我们在混乱的数据中,快速定位关键信息。
下次再遇到这类题,别慌!
打开你的代码编辑器,写下:
const stack = [];
然后,微笑——因为你知道,胜利已在手中。