1. 单调栈算法概述
单调栈(Monotonic Stack)是一种特殊的栈结构,它在算法题解中扮演着重要角色。我第一次接触这个概念是在解决"柱状图中最大矩形"问题时,当时遍历解法的时间复杂度始终无法优化到O(n),直到发现了单调栈这个神奇的工具。
单调栈的核心特性是栈内元素始终保持单调递增或单调递减的顺序。这种看似简单的结构限制,却能在处理数组元素间的大小关系问题时发挥惊人的效果。与普通栈相比,单调栈在元素入栈时会主动破坏单调性的元素弹出,这种"自我净化"机制正是其高效性的关键。
关键理解:单调栈不是一种独立的数据结构,而是对普通栈施加了单调性约束的使用方式。就像给普通数组加上排序规则就变成了优先队列一样。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 单调栈工作原理深度解析
2.1 单调性维护机制
单调栈的核心操作在于入栈时的处理策略。以单调递增栈为例,当新元素准备入栈时:
- 检查栈顶元素是否大于当前元素
- 如果是,则弹出栈顶元素(破坏单调性)
- 重复上述步骤直到栈顶元素≤当前元素或栈空
- 将当前元素入栈
这个过程可以用以下伪代码表示:
python复制stack = []
for num in nums:
while stack and stack[-1] > num: # 维护单调递增
stack.pop()
stack.append(num)
2.2 四种基本变体
单调栈根据单调方向和处理顺序的不同,可分为四种基本类型:
| 类型 | 单调方向 | 处理顺序 | 典型应用场景 |
|---|---|---|---|
| 递增栈 | 栈底→栈顶递增 | 从左到右 | 下一个更大元素 |
| 递减栈 | 栈底→栈顶递减 | 从左到右 | 下一个更小元素 |
| 反向递增栈 | 栈底→栈顶递增 | 从右到左 | 前一个更大元素 |
| 反向递减栈 | 栈底→栈顶递减 | 从右到左 | 前一个更小元素 |
2.3 时间复杂度分析
虽然单调栈有嵌套循环结构,但每个元素最多入栈和出栈各一次,因此总体时间复杂度是O(n)。这比暴力解法的O(n²)有了质的飞跃。
3. 单调栈的经典应用场景
3.1 下一个更大元素问题
LeetCode 496题是典型的单调栈应用题。给定两个数组nums1和nums2,需要找到nums1中每个元素在nums2中的下一个更大元素。
解决方案:
python复制def nextGreaterElement(nums1, nums2):
stack = []
mapping = {}
for num in nums2:
while stack and num > stack[-1]:
mapping[stack.pop()] = num
stack.append(num)
return [mapping.get(num, -1) for num in nums1]
3.2 柱状图最大矩形
LeetCode 84题要求计算柱状图中能勾勒出的最大矩形面积。这是单调栈的经典难题。
关键思路:
- 维护一个单调递增栈
- 当遇到较小元素时,计算以栈顶元素为高的矩形面积
- 注意处理边界情况(前后补0)
实现代码:
python复制def largestRectangleArea(heights):
heights = [0] + heights + [0]
stack = []
max_area = 0
for i in range(len(heights)):
while stack and heights[i] < heights[stack[-1]]:
h = heights[stack.pop()]
w = i - stack[-1] - 1
max_area = max(max_area, h * w)
stack.append(i)
return max_area
3.3 每日温度问题
LeetCode 739题要求对于每天的温度,找到需要等待多少天才能遇到更暖和的温度。
解决方案:
python复制def dailyTemperatures(T):
stack = []
res = [0] * len(T)
for i in range(len(T)):
while stack and T[i] > T[stack[-1]]:
prev = stack.pop()
res[prev] = i - prev
stack.append(i)
return res
4. 单调栈的进阶技巧
4.1 处理循环数组
当问题涉及循环数组时(如LeetCode 503),可以通过将数组长度翻倍来模拟循环效果:
python复制def nextGreaterElements(nums):
n = len(nums)
res = [-1] * n
stack = []
for i in range(2 * n):
while stack and nums[i % n] > nums[stack[-1]]:
res[stack.pop()] = nums[i % n]
if i < n:
stack.append(i)
return res
4.2 二维矩阵中的应用
单调栈可以扩展到二维问题,如LeetCode 85题"最大矩形"。解法是:
- 将矩阵逐行转换为柱状图高度
- 对每行应用柱状图最大矩形算法
4.3 结合其他数据结构
有时需要将单调栈与其他数据结构结合使用。例如在解决"滑动窗口最大值"问题时,可以结合双端队列实现单调队列。
5. 常见错误与调试技巧
5.1 边界条件处理
初学者常犯的错误是忽略边界条件。例如在柱状图问题中,不在数组前后补0,会导致无法正确处理最后一个元素。
5.2 索引与值的混淆
单调栈可以存储索引或值,但两者不能混用。存储索引通常更灵活,可以获取更多信息。
5.3 相等元素的处理
当遇到相等元素时,要根据具体问题决定是否弹出。例如在求下一个更大元素时,相等元素不算"更大",因此不应弹出。
5.4 调试建议
- 打印栈内容:在每次操作后打印栈的状态
- 可视化过程:在纸上画出元素入栈出栈的过程
- 小规模测试:先用简单例子验证算法正确性
6. 单调栈的变种与应用扩展
6.1 单调队列
单调队列是单调栈的扩展,可以在两端操作。常用于滑动窗口相关问题,如LeetCode 239题。
6.2 二维单调性
在解决一些矩阵问题时,可能需要同时维护行和列的单调性,如LeetCode 1504题"统计全1子矩形"。
6.3 结合动态规划
某些动态规划问题可以利用单调栈优化,如LeetCode 975题"奇偶跳"中的优化解法。
7. 实战训练建议
要真正掌握单调栈,建议按以下顺序练习:
- 基础应用:496、739、901
- 经典难题:84、85
- 变种问题:503、42(接雨水)
- 综合应用:907、1124
我在刷题过程中发现,单调栈的问题往往有相似的代码结构。一旦掌握了核心模式,很多问题都能迎刃而解。建议从简单题开始,逐步增加难度,每道题都尝试自己推导解法,而不是直接看答案。
