本文是「数据结构与算法面试精讲」系列第21篇。前20篇覆盖了链表、二叉树、回溯、DP、哈希表、BST、滑动窗口、栈队列、二分查找、双指针、排序、图、前缀和、堆、Trie、并查集、LRU Cache等全部常见数据结构和算法。第21篇,我们来聊一个"栈的进阶用法"------单调栈。它不是一个全新的数据结构,而是一种"模式",一旦掌握,就能解决一大类问题。
一、单调栈基本原理
单调栈的核心思想是:维护一个栈,栈内元素保持单调递增或单调递减,在遍历过程中,当遇到破坏单调性的元素时,触发"结算"操作。
单调递增栈 vs 单调递减栈
| 类型 | 栈内顺序 | 解决的问题 | 典型题目 |
|---|---|---|---|
| 单调递减栈 | 栈底到栈顶递减 | 找下一个更大元素 | 739. 每日温度 |
| 单调递增栈 | 栈底到栈顶递增 | 找下一个更小元素 | 84. 柱状图最大矩形 |
核心模板
// 单调递减栈------找下一个更大元素
for (int i = 0; i < nums.length; i++) {
while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {
int idx = stack.pop();
// 结算:nums[i] 是 nums[idx] 的下一个更大元素
}
stack.push(i);
}
时间复杂度
-
每个元素入栈一次、出栈最多一次
-
O(n),比暴力法的 O(n²) 大幅优化
二、739. 每日温度(Easy)
问题描述
给定一个温度数组,返回需要等多少天才会出现更高的温度。
输入: temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
输出: [1, 1, 4, 2, 1, 1, 0, 0]
Java实现
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] result = new int[n];
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int idx = stack.pop();
result[idx] = i - idx;
}
stack.push(i);
}
return result;
}
}
Python实现
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
result = [0] * n
stack = []
for i in range(n):
while stack and temperatures[i] > temperatures[stack[-1]]:
idx = stack.pop()
result[idx] = i - idx
stack.append(i)
return result
C++实现
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> result(n, 0);
stack<int> st;
for (int i = 0; i < n; i++) {
while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
int idx = st.top(); st.pop();
result[idx] = i - idx;
}
st.push(i);
}
return result;
}
};
三、84. 柱状图中最大的矩形(Hard)
问题描述
给定一个数组,表示柱状图中每个柱子的高度,找出最大矩形面积。
输入: heights = [2, 1, 5, 6, 2, 3]
输出: 10 (由高度5和6组成的5x2矩形)
思路推导
核心洞察:对每个柱子,以它的高度为高,向左向右找到第一个比它矮的柱子,左右边界之间的宽度就是它的最大宽度。
用单调递增栈:遍历时,当当前柱子高度小于栈顶,说明栈顶的右边界找到了,开始结算。
Java实现(哨兵优化)
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] h = new int[n + 2];
System.arraycopy(heights, 0, h, 1, n);
h[0] = 0;
h[n + 1] = 0;
Deque<Integer> stack = new ArrayDeque<>();
stack.push(0);
int maxArea = 0;
for (int i = 1; i <= n + 1; i++) {
while (h[i] < h[stack.peek()]) {
int height = h[stack.pop()];
int width = i - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}
}
Python实现
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
h = [0] + heights + [0]
n = len(h)
stack = [0]
max_area = 0
for i in range(1, n):
while h[i] < h[stack[-1]]:
height = h[stack.pop()]
width = i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
四、单调栈问题分类
| 题目 | 难度 | 类型 | 核心思路 |
|---|---|---|---|
| 739. 每日温度 | Easy | 下一个更大元素 | 单调递减栈 |
| 496. 下一个更大元素 I | Easy | 下一个更大元素 | 单调递减栈 + HashMap |
| 503. 下一个更大元素 II | Medium | 下一个更大元素(循环数组) | 单调递减栈 + 遍历两轮 |
| 84. 柱状图中最大矩形 | Hard | 最大面积 | 单调递增栈 + 哨兵 |
| 85. 最大矩形 | Hard | 最大面积(二维) | 逐行化为84题 |
| 42. 接雨水 | Hard | 面积计算 | 单调栈/双指针 |
五、总结
单调栈的核心公式:
暴力法 O(n²) + 单调栈优化 = O(n)
判断什么时候用单调栈:
-
问题涉及"找下一个更大/更小元素"
-
暴力法需要两层循环
-
内层循环的"终止条件"具有单调性
面试答题顺序:
-
先讲暴力法,分析为什么慢
-
引入单调栈,画图演示
-
写代码实现
-
分析复杂度
参考资料
-
LeetCode 739. Daily Temperatures --- 官方题解
-
LeetCode 84. Largest Rectangle in Histogram --- 官方题解
-
《算法竞赛进阶指南》------单调栈章节