单调栈 完整详解

资料:https://pan.quark.cn/s/43d906ddfa1b、https://pan.quark.cn/s/90ad8fba8347、https://pan.quark.cn/s/d9d72152d3cf

一、单调栈 是什么

单调栈 是一种具有单调性的栈结构,是栈的一个重要变种,核心特征是:栈内的所有元素,始终严格遵守「递增」或「递减」的顺序排列,新元素入栈时会触发特殊的弹出规则,保证这种单调性不被破坏。
核心价值:单调栈可以做到 在 O(n) 线性时间复杂度 内,高效解决数组中「每个元素找左右侧第一个更大/更小元素」的一类经典问题,这类问题暴力解的时间复杂度为 O(n²),单调栈是该类问题的最优解。
补充说明:单调栈本质是「空间换时间」,空间复杂度为 O(n),用栈的存储空间,把原本需要两层循环的暴力解,优化为一次遍历即可完成。

二、单调栈的核心特性

核心定义

栈内元素满足严格的单调性,只有两种核心类型,无其他变种:

  1. 单调递增栈:栈内的元素,从栈底到栈顶 依次严格增大 → 栈顶是当前栈内的最大值
  2. 单调递减栈:栈内的元素,从栈底到栈顶 依次严格减小 → 栈顶是当前栈内的最小值

核心入栈规则 核心重点

这是单调栈的灵魂,所有操作都基于这个规则,新元素入栈必触发该规则,规则统一:
当准备将新元素 num 入栈时,循环判断「栈顶元素」与「新元素 num」的大小关系:

  • 如果栈顶元素不满足当前栈的单调性要求 → 将栈顶元素弹出栈
  • 重复弹出操作,直到栈为空 或 栈顶元素满足单调性要求
  • 最后将新元素 num 压入栈中

两个核心结论 必记

  1. 所有元素只会入栈一次、出栈一次,不会重复入栈和出栈,这是单调栈能做到 O(n) 时间复杂度的核心原因。
  2. 单调栈解决的问题具有极强的专一性:专门解决 数组中元素的「左右侧第一个最值匹配」问题,遇到这类问题,无脑用单调栈即可。

三、单调栈的核心应用场景 两类核心问题

单调栈的应用场景高度固定,笔试面试中99%的考题都围绕这两个场景展开,记住「场景对应栈类型」的规则,解题效率翻倍,无任何例外:

场景一 找数组中每个元素「右侧第一个更大的元素」

✅ 对应栈类型:单调递减栈

场景二 找数组中每个元素「右侧第一个更小的元素」

✅ 对应栈类型:单调递增栈

衍生场景 均为上述两类的变形 规则不变

  1. 找每个元素「左侧第一个更大的元素」
  2. 找每个元素「左侧第一个更小的元素」
  3. 找每个元素「左右侧第一个更大元素的距离」
  4. 找每个元素「左右侧第一个更小元素的距离」

核心规律:「左侧」类问题,从数组左到右遍历;「右侧」类问题,从数组右到左遍历。

四、核心场景实战 含完整可运行代码

所有案例均为 Python 实现,代码为最优解写法,无冗余逻辑,可直接套用,所有案例统一使用测试数组:nums = [2, 1, 2, 4, 3]

前置说明

栈中存储的是数组的「下标」而非元素值,这是单调栈的标准写法、最优写法,原因有2点:

  1. 既可以通过下标获取元素值,也能获取元素的位置信息;
  2. 求解「元素距离」类问题时,必须用到下标计算,存储值无法实现。

实战1 找每个元素 右侧第一个更大的元素

问题描述:遍历数组,对每个元素,返回其右侧第一个比它大的元素,无则返回 -1
测试数组:[2,1,2,4,3] → 正确结果:[4,2,4,-1,-1]
对应栈类型:单调递减栈
遍历顺序:从右向左遍历

def next_greater_element(nums):
    n = len(nums)
    res = [-1] * n  # 初始化结果数组,默认值-1
    stack = []      # 单调递减栈,存下标
    # 从右向左遍历
    for i in range(n-1, -1, -1):
        # 核心规则:栈顶元素对应的值 <= 当前值,弹出栈顶,破坏单调性则弹出
        while stack and nums[stack[-1]] <= nums[i]:
            stack.pop()
        # 栈不为空,栈顶就是右侧第一个更大的元素的下标
        if stack:
            res[i] = nums[stack[-1]]
        # 入栈当前下标
        stack.append(i)
    return res

nums = [2,1,2,4,3]
print(next_greater_element(nums))  # 输出 [4, 2, 4, -1, -1]

实战2 找每个元素 右侧第一个更小的元素

问题描述:遍历数组,对每个元素,返回其右侧第一个比它小的元素,无则返回 -1
测试数组:[2,1,2,4,3] → 正确结果:[1,-1,-1,3,-1]
对应栈类型:单调递增栈
遍历顺序:从右向左遍历

def next_smaller_element(nums):
    n = len(nums)
    res = [-1] * n
    stack = []  # 单调递增栈,存下标
    for i in range(n-1, -1, -1):
        # 核心规则:栈顶元素对应的值 >= 当前值,弹出栈顶
        while stack and nums[stack[-1]] >= nums[i]:
            stack.pop()
        if stack:
            res[i] = nums[stack[-1]]
        stack.append(i)
    return res

nums = [2,1,2,4,3]
print(next_smaller_element(nums))  # 输出 [1, -1, -1, 3, -1]

实战3 找每个元素 左侧第一个更大的元素

问题描述:遍历数组,对每个元素,返回其左侧第一个比它大的元素,无则返回 -1
测试数组:[2,1,2,4,3] → 正确结果:[-1,2,-1,-1,4]
对应栈类型:单调递减栈
遍历顺序:从左向右遍历

def prev_greater_element(nums):
    n = len(nums)
    res = [-1] * n
    stack = []  # 单调递减栈,存下标
    # 从左向右遍历
    for i in range(n):
        while stack and nums[stack[-1]] <= nums[i]:
            stack.pop()
        if stack:
            res[i] = nums[stack[-1]]
        stack.append(i)
    return res

nums = [2,1,2,4,3]
print(prev_greater_element(nums))  # 输出 [-1, 2, -1, -1, 4]

实战4 找每个元素 左侧第一个更小的元素

问题描述:遍历数组,对每个元素,返回其左侧第一个比它小的元素,无则返回 -1
测试数组:[2,1,2,4,3] → 正确结果:[-1,-1,1,2,2]
对应栈类型:单调递增栈
遍历顺序:从左向右遍历

def prev_smaller_element(nums):
    n = len(nums)
    res = [-1] * n
    stack = []  # 单调递增栈,存下标
    for i in range(n):
        while stack and nums[stack[-1]] >= nums[i]:
            stack.pop()
        if stack:
            res[i] = nums[stack[-1]]
        stack.append(i)
    return res

nums = [2,1,2,4,3]
print(prev_smaller_element(nums))  # 输出 [-1, -1, 1, 2, 2]

五、单调栈 经典高频面试题 接雨水 柱状图中最大的矩形

这两道题是单调栈的天花板级经典考题,笔试面试必考,本质都是「左右侧第一个最值」问题的组合应用,掌握后即可吃透单调栈的所有考点,两道题均为 困难级 但有固定解题模板。

经典题1 接雨水

问题描述:给定一个非负整数数组表示柱子高度,数组元素围成的容器能接多少雨水
核心思路:对每个柱子,能接的雨水量 = 「左侧第一个更高柱子高度」和「右侧第一个更高柱子高度」的最小值 - 当前柱子高度,累加所有柱子的接水量即可。
对应栈类型:单调递减栈

def trap(height):
    res = 0
    stack = []  # 单调递减栈,存下标
    n = len(height)
    for i in range(n):
        # 栈顶高度 < 当前高度,形成凹槽,可接水
        while stack and height[stack[-1]] < height[i]:
            cur = stack.pop()  # 凹槽底部的柱子下标
            if not stack:
                break
            left = stack[-1]   # 左侧第一个更高的柱子
            width = i - left - 1
            high = min(height[left], height[i]) - height[cur]
            res += width * high
        stack.append(i)
    return res

height = [0,1,0,2,1,0,1,3,2,1,2,1]
print(trap(height))  # 输出 6

经典题2 柱状图中最大的矩形

问题描述:给定非负整数数组表示柱状图高度,每个柱子宽度为1,求柱状图中能勾勒出的最大矩形面积
核心思路:对每个柱子,能构成的最大矩形宽度 = 「右侧第一个更矮柱子下标」 - 「左侧第一个更矮柱子下标」 - 1,面积 = 宽度 × 当前高度,取所有面积的最大值即可。
对应栈类型:单调递增栈

def largest_rectangle_area(heights):
    # 数组首尾补0,简化边界处理
    heights = [0] + heights + [0]
    stack = []
    max_area = 0
    for i in range(len(heights)):
        # 栈顶高度 > 当前高度,计算栈顶柱子的最大面积
        while stack and heights[stack[-1]] > heights[i]:
            cur = stack.pop()
            # 栈顶弹出后的新栈顶,就是左侧第一个更矮的柱子
            width = i - stack[-1] - 1
            area = heights[cur] * width
            max_area = max(max_area, area)
        stack.append(i)
    return max_area

heights = [2,1,5,6,2,3]
print(largest_rectangle_area(heights))  # 输出 10

六、算法复杂度分析

时间复杂度 O(n)

数组的每个元素只会入栈一次,出栈一次,没有任何元素会被重复处理,遍历的过程中,所有入栈和出栈操作的总次数为 O(n),因此整体时间复杂度是严格的线性阶。
对比暴力解法的 O(n²),单调栈的效率有质的提升,数组规模越大,优势越明显。

空间复杂度 O(n)

额外开辟了一个栈空间存储数组下标,最坏情况下(数组完全单调),所有元素都会存入栈中,此时栈的大小等于数组长度,空间复杂度为 O(n)。
这是「空间换时间」的典型体现,也是该类问题的最优空间复杂度。

七、关键细节 避坑指南 笔试必看

细节1 栈中存下标 而非元素值

这是最基础也是最重要的细节,是单调栈的标准写法,原因前文已说明,记住:所有单调栈题目,栈里一律存下标,不要存值,避免后续无法求解。

细节2 单调性的边界处理 严格/非严格

单调栈的单调性分为「严格单调」和「非严格单调」,对应入栈规则的细微区别:

  • 严格单调递增栈:入栈规则是 nums[栈顶] >= 当前值 时弹出;
  • 非严格单调递增栈:入栈规则是 nums[栈顶] > 当前值 时弹出;

结论:笔试面试中,一律用严格单调,可以避免处理重复元素时的逻辑错误,兼容性最强。

细节3 遍历方向的选择

固定规则,无例外,必须记牢:

  • 求「左侧」第一个最值 → 从 左 到 右 遍历数组;
  • 求「右侧」第一个最值 → 从 右 到 左 遍历数组。

细节4 结果数组的初始化值

求「更大/更小元素」类问题时,结果数组的初始化值统一为 -1,因为题目中一般要求「无匹配元素则返回-1」;求「面积/雨水」类问题时,初始化值为 0 即可。

细节5 处理环形数组问题

如果题目是环形数组(数组首尾相连),只需将原数组拼接一次(nums = nums + nums),再用单调栈求解即可,其余逻辑不变。

八、核心总结

  1. 单调栈 是栈的变种,核心是栈内元素保持「递增」或「递减」的严格单调性;
  2. 核心入栈规则:新元素入栈前,弹出所有破坏单调性的栈顶元素,保证栈的性质不变;
  3. 核心价值:O(n) 时间复杂度解决「左右侧第一个更大/更小元素」问题,最优解;
  4. 核心规则:找更大用递减栈,找更小用递增栈;左侧左遍历,右侧右遍历;栈中存下标;
  5. 元素只会入栈出栈一次,是线性时间复杂度的核心原因;
  6. 经典考题:接雨水、柱状图最大矩形,均为单调栈的组合应用,有固定解题模板;
  7. 本质是「空间换时间」,用栈的O(n)空间,换暴力解的O(n²)时间。

更多推荐