🌪️ 单调栈:力扣“接雨水”、“找温度”、“算面积”的终极武器!

7 阅读4分钟

别再被“下一个更大元素”吓到!一文搞懂单调栈,从此刷题如喝水!


🎯 你是否也曾被这些题目折磨过?

是不是一看就头大?别慌!这些看似八竿子打不着的问题,其实都藏着同一个“幕后黑手”——单调栈(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 = [];

然后,微笑——因为你知道,胜利已在手中。