算法技巧:单调栈
单调栈 完整详解
资料:https://pan.quark.cn/s/43d906ddfa1b、https://pan.quark.cn/s/90ad8fba8347、https://pan.quark.cn/s/d9d72152d3cf
一、单调栈 是什么
单调栈 是一种具有单调性的栈结构,是栈的一个重要变种,核心特征是:栈内的所有元素,始终严格遵守「递增」或「递减」的顺序排列,新元素入栈时会触发特殊的弹出规则,保证这种单调性不被破坏。
核心价值:单调栈可以做到 在 O(n) 线性时间复杂度 内,高效解决数组中「每个元素找左右侧第一个更大/更小元素」的一类经典问题,这类问题暴力解的时间复杂度为 O(n²),单调栈是该类问题的最优解。
补充说明:单调栈本质是「空间换时间」,空间复杂度为 O(n),用栈的存储空间,把原本需要两层循环的暴力解,优化为一次遍历即可完成。
二、单调栈的核心特性
核心定义
栈内元素满足严格的单调性,只有两种核心类型,无其他变种:
- 单调递增栈:栈内的元素,从栈底到栈顶 依次严格增大 → 栈顶是当前栈内的最大值
- 单调递减栈:栈内的元素,从栈底到栈顶 依次严格减小 → 栈顶是当前栈内的最小值
核心入栈规则 核心重点
这是单调栈的灵魂,所有操作都基于这个规则,新元素入栈必触发该规则,规则统一:
当准备将新元素 num 入栈时,循环判断「栈顶元素」与「新元素 num」的大小关系:
- 如果栈顶元素不满足当前栈的单调性要求 → 将栈顶元素弹出栈
- 重复弹出操作,直到栈为空 或 栈顶元素满足单调性要求
- 最后将新元素
num压入栈中
两个核心结论 必记
- 所有元素只会入栈一次、出栈一次,不会重复入栈和出栈,这是单调栈能做到 O(n) 时间复杂度的核心原因。
- 单调栈解决的问题具有极强的专一性:专门解决 数组中元素的「左右侧第一个最值匹配」问题,遇到这类问题,无脑用单调栈即可。
三、单调栈的核心应用场景 两类核心问题
单调栈的应用场景高度固定,笔试面试中99%的考题都围绕这两个场景展开,记住「场景对应栈类型」的规则,解题效率翻倍,无任何例外:
场景一 找数组中每个元素「右侧第一个更大的元素」
✅ 对应栈类型:单调递减栈
场景二 找数组中每个元素「右侧第一个更小的元素」
✅ 对应栈类型:单调递增栈
衍生场景 均为上述两类的变形 规则不变
- 找每个元素「左侧第一个更大的元素」
- 找每个元素「左侧第一个更小的元素」
- 找每个元素「左右侧第一个更大元素的距离」
- 找每个元素「左右侧第一个更小元素的距离」
核心规律:「左侧」类问题,从数组左到右遍历;「右侧」类问题,从数组右到左遍历。
四、核心场景实战 含完整可运行代码
所有案例均为 Python 实现,代码为最优解写法,无冗余逻辑,可直接套用,所有案例统一使用测试数组:nums = [2, 1, 2, 4, 3]
前置说明
栈中存储的是数组的「下标」而非元素值,这是单调栈的标准写法、最优写法,原因有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),再用单调栈求解即可,其余逻辑不变。
八、核心总结
- 单调栈 是栈的变种,核心是栈内元素保持「递增」或「递减」的严格单调性;
- 核心入栈规则:新元素入栈前,弹出所有破坏单调性的栈顶元素,保证栈的性质不变;
- 核心价值:O(n) 时间复杂度解决「左右侧第一个更大/更小元素」问题,最优解;
- 核心规则:找更大用递减栈,找更小用递增栈;左侧左遍历,右侧右遍历;栈中存下标;
- 元素只会入栈出栈一次,是线性时间复杂度的核心原因;
- 经典考题:接雨水、柱状图最大矩形,均为单调栈的组合应用,有固定解题模板;
- 本质是「空间换时间」,用栈的O(n)空间,换暴力解的O(n²)时间。
更多推荐



所有评论(0)