单调栈

单调栈总纲:最近更大/更小、左右边界、模板方向判断与经典题型总结。

#resource / algorithm #type / concept #status / growing #algo / monotonic-stack #ds / stack

[!info] related notes 算法面试题型 MOC 每日温度 柱状图中最大的矩形 接雨水

单调栈

单调栈的本质是:

在遍历数组时,维护一批“仍然可能成为答案的候选元素”,并且让这些候选在栈里保持单调。

一句话记忆:

  • 单调递增栈:从栈底到栈顶递增
  • 单调递减栈:从栈底到栈顶递减
  • 最常见用途:最近更大、最近更小、左右边界

一眼判断:这题是不是单调栈

看到下面这些信号,要往单调栈想:

  • 求某个位置左边 / 右边第一个更大元素
  • 求某个位置左边 / 右边第一个更小元素
  • 要为每个元素确定左右边界
  • 当前元素到来时,会淘汰一批不可能再成为答案的旧元素
  • 题目在强调“最近满足大小关系的元素”

如果你发现:

  • 当前元素会一次性解决掉前面多个位置的答案
  • 被弹出的元素以后再也不可能更优
  • 题目答案和大小比较、区间边界强相关

那大概率就是单调栈。

单调栈和普通栈的区别

普通栈更关心:

  • 匹配
  • 消除
  • 碰撞
  • 表达式上下文

单调栈更关心:

  • 比较关系
  • 候选淘汰
  • 边界确定

也就是说:

  • 普通栈处理“关系”
  • 单调栈处理“比较”

单调栈在维护什么

它维护的不是“所有元素”,而是:

到当前为止,还没有失去资格的候选元素。

当新元素 x 到来时:

  • 有些旧元素会被证明不可能再成为答案,于是出栈
  • 剩下的元素仍然有资格,于是继续留在栈中
  • 当前元素自己也可能成为后续候选,于是入栈

所以单调栈本质上是在做:

剪枝 + 延迟结算

做单调栈的固定五问

每道题先强制回答下面五个问题:

1. 我要找左边还是右边

  • 找右边:常见写法是从左到右遍历,边走边给旧元素结算
  • 找左边:常见写法是从左到右遍历,先弹不合法元素,再看栈顶

2. 我要找更大还是更小

别先背名字,先想:

  • 当前元素到来时,谁应该被弹掉?

这是方向判断的核心。

3. 栈里存值还是存下标

大多数单调栈题都存下标,因为通常需要:

  • 计算距离
  • 回填答案
  • 访问原数组值
  • 确定左右边界

4. 弹栈时是在结算答案,还是只是删除无效候选

有些题一弹就能算答案,比如每日温度。

有些题弹栈只是清理不合法元素,比如求左边最近更大 / 更小时。

5. 需要哨兵吗

像柱状图最大矩形这类题,常常通过:

  • 首尾补哨兵
  • 最后统一清栈

来减少边界判断。

一页式速查表

目标常见遍历方向常见维护方式弹栈含义代表题
右边第一个更大从左到右单调递减栈当前元素帮旧元素结算答案[[daily-temperatures
右边第一个更小从左到右单调递增栈当前元素帮旧元素结算答案[[largest-rectangle-in-histogram
左边第一个更大从左到右弹掉不够大的元素栈顶就是答案最近更大元素类题
左边第一个更小从左到右弹掉不够小的元素栈顶就是答案最近更小元素类题
凹槽 / 边界结算从左到右按高度维护单调性弹出的元素作为中间层或底部接雨水

四种最常见模型

1. 求右边第一个更大元素

维护单调递减栈,栈里存下标。

当前元素更大时,说明它就是栈顶元素右边第一个更大值。

const ans = new Array(n).fill(-1);
const stack = [];

for (let i = 0; i < n; i++) {
    while (stack.length && nums[i] > nums[stack[stack.length - 1]]) {
        const j = stack.pop();
        ans[j] = i;
    }
    stack.push(i);
}

代表题:每日温度、下一个更大元素 I / II。

2. 求右边第一个更小元素

维护单调递增栈,栈里存下标。

当前元素更小时,说明它就是栈顶元素右边第一个更小值。

const ans = new Array(n).fill(-1);
const stack = [];

for (let i = 0; i < n; i++) {
    while (stack.length && nums[i] < nums[stack[stack.length - 1]]) {
        const j = stack.pop();
        ans[j] = i;
    }
    stack.push(i);
}

代表题:柱状图中最大的矩形

3. 求左边第一个更大 / 更小元素

这一类常见思路不是“边走边给别人结算”,而是:

  • 当前元素入栈前,先弹掉不合法候选
  • 弹完以后,栈顶就是左边最近合法元素
const left = new Array(n).fill(-1);
const stack = [];

for (let i = 0; i < n; i++) {
    while (stack.length && invalid(nums[stack[stack.length - 1]], nums[i])) {
        stack.pop();
    }

    if (stack.length) {
        left[i] = stack[stack.length - 1];
    }

    stack.push(i);
}

4. 凹槽 / 区间边界结算

这类题弹栈时,弹出的元素往往不是答案本身,而是“中间层”。

最典型就是 接雨水

  • 被弹出的柱子是凹槽底部
  • 新栈顶和当前柱子组成左右边界

每日温度

题目本质:

求每个位置右边第一个更大元素的位置差。

为什么栈里放下标而不是温度值:

  • 因为最后要计算距离
  • 温度值可以通过下标回原数组取

代码模板:

var dailyTemperatures = function(temperatures) {
    const n = temperatures.length;
    const ans = new Array(n).fill(0);
    const stack = [];

    for (let i = 0; i < n; i++) {
        while (stack.length && temperatures[i] > temperatures[stack[stack.length - 1]]) {
            const j = stack.pop();
            ans[j] = i - j;
        }
        stack.push(i);
    }

    return ans;
};

更完整题解见 每日温度

柱状图中最大的矩形

题目本质:

对每个柱子,求它左边和右边第一个更小元素,从而得到最大可扩宽度。

关键理解:

  • 弹栈时,当前元素是右边界
  • 弹栈后的新栈顶是左边界
  • 被弹出的柱子是当前确定面积的高度

更完整题解见 柱状图中最大的矩形

接雨水

题目本质:

找到一个凹槽的左边界、底部和右边界,然后结算这段水量。

关键理解:

  • 栈保持单调递减
  • 当前柱子更高时,尝试填平中间低洼
  • 弹栈得到底部,左右边界由当前柱子和新栈顶决定

更完整题解见 接雨水

为什么 while 这么重要

很多人写单调栈时最容易犯的错,就是把 while 写成 if

但单调栈里,新元素经常会连续淘汰多个旧元素。

比如:

  • 每日温度中,一个更高温度可能一次性帮好几天结算答案
  • 柱状图中,一个更矮柱子可能连续让多个更高柱子出栈
  • 接雨水中,一个更高柱子可能连续填多个低洼结构

所以单调栈很多时候的本质就是:

当前元素触发了一次批量结算。

单调栈常见错误

1. 方向记反

不要死背“递增栈 / 递减栈”的名字,直接想:

  • 我想让谁出栈?
  • 出栈条件是 > 还是 <

2. 该存下标却存了值

一旦涉及距离、边界、答案数组,基本都应该存下标。

3. 该用 while 却只写了 if

这会漏掉连续弹栈的情况。

4. 相等时处理不清楚

有些题要求严格大于,有些题允许大于等于。

所以比较符号必须和题意对应:

  • > 还是 >=
  • < 还是 <=

这一点会直接影响去重和边界归属。

5. 边界没有统一

能补哨兵就补哨兵,能统一清栈就统一清栈。

怎么系统学习单调栈

第 1 阶段:先吃透“右边第一个更大”

建议先做:

  • 每日温度
  • 下一个更大元素 I
  • 下一个更大元素 II

目标:

  • 理解为什么当前元素能帮旧元素结算答案
  • 理解为什么通常存下标

第 2 阶段:再做边界题

建议再做:

目标:

  • 理解弹栈时不仅能结算单点答案,也能结算区间信息
  • 理解哨兵和统一边界处理

第 3 阶段:做更多变形题

建议补:

  • 最大矩形
  • 子数组最小值之和
  • 最小字典序 / 去重类单调栈

面试里怎么解释“为什么这题用单调栈”

可以直接按这个模板答:

  1. 这题本质是在找每个元素最近的更大 / 更小元素或边界
  2. 当前元素到来时,会淘汰一批不可能再成为答案的候选
  3. 被淘汰的元素以后不需要再看,所以适合用单调栈维护候选集

一段总结

单调栈不是单纯“背模板”的题型,而是一种利用大小关系做候选淘汰和批量结算的思维方式。

  • 找最近更大 / 更小 -> 先想单调栈
  • 要确定左右边界 -> 先想单调栈
  • 写题时先想清楚:我要弹掉谁,弹掉后答案怎么结算

把这三点练熟,很多看起来不同的题都会收敛成同一个框架。

创建于 2025/1/1 更新于 2026/5/27