← 返回博客列表

一、什么是单调栈

单调栈(Monotone Stack)是一种特殊的栈结构,它在普通栈"后进先出(LIFO)"的基础上,额外要求栈内元素保持单调性(单调递增或单调递减)。

一句话定义
单调栈 = 普通栈 + "入栈时先把破坏单调性的栈顶元素弹出去"这一条规则。

它的典型应用场景是:在一组数据中,对每个元素找"下一个比它大/小的元素"。这类问题如果用暴力双层循环是 O(n²),用单调栈可以降到 O(n)。

1.1 两种单调栈

类型 栈内单调性 用于求解 入栈时弹出条件
单调递增栈 从栈底到栈顶递增 下一个更小元素 栈顶 ≥ 当前值时弹出
单调递减栈 从栈底到栈顶递减 下一个更大元素 栈顶 ≤ 当前值时弹出
⚠ 命名约定
有些教材把"栈底到栈顶递减"称为"单调递减栈",有些则按"弹出的元素是递减的"来命名。本文统一采用"栈内元素的单调性"来命名:栈内递增 = 单调递增栈。

二、核心原理与图解

理解单调栈的关键,是理解"弹栈的瞬间发生了什么"。当一个新元素入栈时,它会"击落"所有比它弱(破坏单调性)的栈顶元素——被击落的元素,它们的"下一个更大/小元素"恰好就是这个新元素。

2.1 一个完整的例子

以数组 [2, 1, 2, 4, 3] 为例,求每个元素右边第一个比它大的元素。我们用单调递减栈(栈内从底到顶递减),栈中存的是元素下标。

单调栈工作过程:求每个元素右边第一个更大元素 数组: 2 1 2 4 3 步骤 1:处理下标 0,值 = 2 栈为空,直接入栈 0 栈: [0] 步骤 2:处理下标 1,值 = 1 栈顶值 2 > 1,保持递减,直接入栈 0 1 栈: [0, 1] 步骤 3:处理下标 2,值 = 2 栈顶值 1 ≤ 2,弹出下标 1,记录 ans[1]=2 1↓ ans[1] = 2 (下标 2) 栈顶值 2 ≤ 2,弹出下标 0?不,2 不小于 2,停止 2 入栈 0 2 栈: [0, 2] 步骤 4:处理下标 3,值 = 4 栈顶值 2 ≤ 4,弹 2,ans[2]=3 栈顶值 2 ≤ 4,弹 0,ans[0]=3 栈空,4 入栈 3 栈: [3] 步骤 5:处理下标 4,值 = 3 栈顶值 4 > 3,保持递减,3 入栈 3 4 栈: [3, 4] 收尾:栈中剩余 [3, 4] ans[3] = -1(右边没有更大) ans[4] = -1(右边没有更大) 最终结果: ans = [3, 2, 3, -1, -1] 含义:下标 0 的右边第一个更大值在下标 3;下标 1 在下标 2;下标 2 在下标 3;下标 3、4 右边没有更大的
图 1:单调递减栈求"下一个更大元素"的完整过程

2.2 复杂度分析

⏱ 时间复杂度:O(n)
虽然有嵌套循环的样子,但每个元素最多入栈一次、出栈一次,所以总操作数为 2n,是线性复杂度。

💾 空间复杂度:O(n)
最坏情况下栈里同时存在 n 个元素(数组本身就是单调的)。

三、通用模板

掌握下面这个模板,90% 的单调栈题目都能套用:

// 单调栈通用模板:求每个元素右边第一个更大元素的下标
// 若不存在返回 -1
vector<int> nextGreaterElement(const vector<int>& nums) {
    int n = nums.size();
    vector<int> ans(n, -1);
    stack<int> st;  // 存下标,栈内对应值从底到顶递减

    for (int i = 0; i < n; ++i) {
        // 当前值比栈顶值大,栈顶找到了"下一个更大"
        while (!st.empty() && nums[st.top()] < nums[i]) {
            ans[st.top()] = i;   // 或填 nums[i],看题目要求
            st.pop();
        }
        st.push(i);
    }
    return ans;
}
模板三要点
1. 栈里存下标,不存值——这样既能拿到值比较,也能往 ans 里写下标
2. while 循环条件用 < 还是 ≤——取决于"相等算不算更大",通常用 <(相等不入栈即视为同一档)
3. 循环结束后栈中剩下的元素——它们右边没有更大元素,ans 保持初始值 -1

四、示例一:每日温度

题目(LeetCode 739):给定一个数组 temperatures,表示每天的温度,返回一个数组 answer,其中 answer[i] 是第 i 天之后第一个更暖和的天数距离第 i 天的天数。如果没有更暖和的一天,填 0。

4.1 示例

输入:[73, 74, 75, 71, 69, 72, 76, 73]
输出:[1, 1, 4, 2, 1, 1, 0, 0]

每日温度:等待天数可视化 73 第0天 →1天 74 第1天 →1天 75 第2天 →4天 71 第3天 →2天 69 第4天 →1天 72 第5天 →1天 76 第6天 →0 73 第7天 →0 橙色越深表示温度越高;箭头表示要等几天才遇到更暖的一天
图 2:每日温度问题——等待更暖和的天气

4.2 C++ 代码

#include <vector>
#include <stack>
using namespace std;

vector<int> dailyTemperatures(const vector<int>& T) {
    int n = T.size();
    vector<int> ans(n, 0);
    stack<int> st;  // 存下标,栈内温度从底到顶递减

    for (int i = 0; i < n; ++i) {
        // 当前温度比栈顶那天的温度高,栈顶那天找到了"更暖和的一天"
        while (!st.empty() && T[st.top()] < T[i]) {
            int prevDay = st.top();
            ans[prevDay] = i - prevDay;  // 距离 = 当前下标 - 之前下标
            st.pop();
        }
        st.push(i);
    }
    return ans;
}

这个例子和通用模板几乎一样,唯一区别是 ans 存的是"距离天数"而非下标,所以多了一步 i - prevDay。

五、示例二:下一个更大元素 II(环形数组)

题目(LeetCode 503):给定一个循环数组 nums(最后一个元素的下一个是第一个),返回每个元素的下一个更大元素,如果不存在填 -1。

5.1 思路:环形展开为线性

处理环形数组的常用技巧是"拉长两倍"——把数组复制一份接在后面,遍历 [0, 2n),用模运算访问原数组。这样每个元素都有机会"绕一圈"找到自己的下一个更大元素。

环形数组拉长为 2 倍线性数组 原数组 (环形): 1 2 1 3 ↑起点 → 拉长 2 倍后 (线性): 1 2 1 3 1 2 1 3 遍历 0~2n-1,用 i%n 访问原数组;只在前 n 个位置写答案
图 3:环形数组拉长两倍处理技巧

5.2 C++ 代码

#include <vector>
#include <stack>
using namespace std;

vector<int> nextGreaterElements(const vector<int>& nums) {
    int n = nums.size();
    vector<int> ans(n, -1);
    stack<int> st;

    // 关键:遍历 2n 次,用 i%n 取真实下标
    for (int i = 0; i < 2 * n; ++i) {
        int idx = i % n;
        while (!st.empty() && nums[st.top()] < nums[idx]) {
            ans[st.top()] = nums[idx];
            st.pop();
        }
        // 只在前 n 次入栈,避免重复入栈
        if (i < n) st.push(idx);
    }
    return ans;
}
💡 关键细节
if (i < n) st.push(idx);——只在前 n 次入栈。如果让第二轮也入栈,会产生重复下标,破坏逻辑。这是环形问题最常见的坑。

六、示例三:柱状图中最大的矩形

题目(LeetCode 84):给定 n 个非负整数,表示柱状图中各柱子的高度,每根柱子宽度为 1,求能组成的最大矩形面积。

6.1 问题分析

最大矩形一定"以某根柱子高度为高,向左右扩展到第一个比它矮的柱子为止"。所以问题转化为:对每根柱子,找左右两边第一个比它矮的位置——典型的单调栈问题。

柱状图最大矩形:以第 3 根(高 5)为基准 2 1 5 6 2 3 以高 6 为基准 宽 1,面积 6 以高 5 为基准 宽 2,面积 10 对每根柱子,找左右第一个比它矮的柱子,宽度即为扩展范围
图 4:柱状图最大矩形——以每根柱子高度为基准向两侧扩展

6.2 C++ 代码

#include <vector>
#include <stack>
using namespace std;

int largestRectangleArea(const vector<int>& heights) {
    int n = heights.size();
    stack<int> st;  // 单调递增栈(栈内高度从底到顶递增)
    int maxArea = 0;

    // 在末尾加一个虚拟高度 0,强制把栈里所有柱子弹出收尾
    for (int i = 0; i <= n; ++i) {
        int h = (i == n) ? 0 : heights[i];  // 哨兵
        while (!st.empty() && heights[st.top()] > h) {
            int height = heights[st.top()];
            st.pop();
            // 左边界:栈顶下面的那个,若栈空则左边界为 -1
            int left = st.empty() ? -1 : st.top();
            int width = i - left - 1;
            maxArea = max(maxArea, height * width);
        }
        st.push(i);
    }
    return maxArea;
}
💡 两个关键技巧
1. 哨兵:在末尾加一个虚拟高度 0,强制把栈里剩余元素全部弹出,避免循环结束后单独处理
2. 左边界来自栈顶下面的元素:弹栈后,新的栈顶就是当前柱子左边第一个比它矮的柱子。这是单调栈"栈内顺序即左右边界关系"的妙用

七、总结与适用场景

单调栈核心要点

维度 要点
本质 用栈维护单调性,在弹栈瞬间记录"下一个更大/小元素"
时间复杂度 O(n)——每个元素入栈出栈各一次
空间复杂度 O(n)——最坏栈存所有元素
识别信号 题目问"下一个更大/更小""最近的高/低""能扩展到的边界"
常见变体 环形数组(拉长 2 倍)、二维(柱状图)、字典序最小(结合贪心)

典型题目清单

🎯 记忆口诀
"找更大用递减栈,找更小用递增栈;弹栈瞬间记答案,栈中剩余填 -1。"
掌握这一句,单调栈的骨架就立住了。