单调栈一文深度解析
单调栈一文深度解析
- 一、单调栈基础概念
- 1.1 定义与核心特性
- 1.2 与普通栈的区别
- 1.3 核心操作模板
- 二、单调栈核心原理
- 2.1 单调性维护机制
- 2.2 典型应用场景
- 三、经典案例解析
- 下一个更大元素
- 问题描述
- 思路分析
- 代码实现
- 复杂度分析
- 接雨水问题
- 问题描述
- 思路分析
- 代码实现
- 复杂度分析
- 柱状图最大面积
- 问题描述
- 思路分析
- 代码实现
- 复杂度分析
- 四、单调栈解题模板
- 4.1 通用解题步骤
- 4.2 常见问题映射表
- 五、进阶技巧与优化
- 5.1 双向单调栈
- 5.2 单调栈与动态规划结合
- 5.3 注意事项
单调栈是一种专门处理元素间序关系
的高效数据结构,它通过维护栈内元素的单调性,能够在线性时间内解决诸如下一个更大/小元素
系列经典问题。本文我将从单调栈的核心原理出发,结合经典案例,解析并总结其解题模板及优化技巧,帮你掌握这一高效解题工具。
一、单调栈基础概念
1.1 定义与核心特性
单调栈是指栈内元素(从栈底到栈顶)保持单调递增或单调递减的栈结构。根据单调性不同,分为:
- 单调递增栈:栈内元素始终满足
stack[i] <= stack[i+1]
- 单调递减栈:栈内元素始终满足
stack[i] >= stack[i+1]
其核心特性是:当新元素入栈时,通过弹出栈顶不满足单调性的元素,确保栈内元素始终保持单调。这种特性使得单调栈能够高效处理「元素与周边元素的序关系」问题。
1.2 与普通栈的区别
特性 | 普通栈 | 单调栈 |
---|---|---|
元素顺序 | 无约束 | 严格单调 |
主要操作 | push/pop | 维护单调性 |
典型场景 | 表达式求值 | 序关系问题 |
时间复杂度 | 不定 | 均摊O(n) |
1.3 核心操作模板
// 单调递增栈模板
Stack<Integer> stack = new Stack<>();
for (int num : nums) {while (!stack.isEmpty() && stack.peek() >= num) { // 破坏递增性时弹出int poped = stack.pop();// 处理弹出元素}stack.push(num); // 新元素入栈,确保栈递增
}// 单调递减栈模板
Stack<Integer> stack = new Stack<>();
for (int num : nums) {while (!stack.isEmpty() && stack.peek() <= num) { // 破坏递减性时弹出int poped = stack.pop();// 处理弹出元素}stack.push(num); // 新元素入栈,确保栈递减
}
二、单调栈核心原理
2.1 单调性维护机制
- 入栈规则:新元素入栈前,弹出所有破坏单调性的栈顶元素
- 元素处理:弹出元素时,当前新元素即为其「下一个更大/更小元素」
- 均摊复杂度:每个元素最多入栈/出栈一次,总时间复杂度O(n)
2.2 典型应用场景
- 下一个更大元素:数组中每个元素右侧第一个比它大的元素
- 接雨水问题:计算柱状图能接的雨水总量
- 柱状图最大面积:求解直方图中的最大矩形面积
- 每日温度:计算每天需要等待多久才会升温
- 股票买卖问题:寻找最佳买卖时机的变种问题
三、经典案例解析
下一个更大元素
问题描述
给定数组nums,返回一个新数组,其中每个元素是原数组对应位置元素的下一个更大元素(右侧第一个比它大的元素,不存在则为-1)。
思路分析
- 使用单调递增栈,栈中保存尚未找到下一个更大元素的元素下标
- 遍历数组时,新元素与栈顶元素比较:
- 若新元素更大:栈顶元素的下一个更大元素即为当前元素,弹出并记录
- 否则:新元素下标入栈
代码实现
public int[] nextGreaterElement(int[] nums) {int[] res = new int[nums.length];Stack<Integer> stack = new Stack<>(); // 存储下标,栈中元素对应的值单调递增for (int i = nums.length - 1; i >= 0; i--) {while (!stack.isEmpty() && nums[stack.peek()] <= nums[i]) {stack.pop(); // 弹出不满足递增的元素}res[i] = stack.isEmpty() ? -1 : nums[stack.peek()];stack.push(i);}return res;
}
复杂度分析
- 时间复杂度:O(n),每个元素入栈出栈各一次
- 空间复杂度:O(n),栈空间最多存储n个元素
接雨水问题
问题描述
给定n个非负整数表示柱状图的高度,计算下雨后能接多少雨水。
思路分析
- 单调栈解法:维护单调递减栈,栈中存储柱子下标
- 当新柱子高度 >= 栈顶柱子高度时,弹出栈顶,计算中间凹槽的接水量
- 接水量由左右边界的较小值减去中间柱子高度决定
代码实现
public int trap(int[] height) {int res = 0;Stack<Integer> stack = new Stack<>(); // 存储下标,栈中高度单调递减for (int i = 0; i < height.length; i++) {while (!stack.isEmpty() && height[i] >= height[stack.peek()]) {int mid = stack.pop();if (stack.isEmpty()) break; // 没有左边界,无法形成凹槽int left = stack.peek();int width = i - left - 1;int h = Math.min(height[left], height[i]) - height[mid];res += width * h;}stack.push(i);}return res;
}
复杂度分析
- 时间复杂度:O(n),每个元素入栈出栈各一次
- 空间复杂度:O(n),栈空间最多存储n个元素
柱状图最大面积
问题描述
给定直方图的高度数组,求其中最大矩形面积。
思路分析
- 单调递增栈:栈中存储下标,对应高度单调递增
- 每个元素出栈时,视为矩形的高,宽度由左右边界决定:
- 左边界:栈顶元素(弹出后的新栈顶)
- 右边界:当前元素下标
- 宽度 = 右边界 - 左边界 - 1
代码实现
public int largestRectangleArea(int[] heights) {int res = 0;Stack<Integer> stack = new Stack<>(); // 存储下标,栈中高度单调递增int[] newHeights = Arrays.copyOf(heights, heights.length + 2); // 首尾加0,避免边界处理for (int i = 0; i < newHeights.length; i++) {while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {int h = newHeights[stack.pop()];int w = i - stack.peek() - 1;res = Math.max(res, h * w);}stack.push(i);}return res;
}
复杂度分析
- 时间复杂度:O(n),每个元素入栈出栈各一次
- 空间复杂度:O(n),栈空间及辅助数组
四、单调栈解题模板
4.1 通用解题步骤
- 确定单调性:根据问题确定使用递增栈还是递减栈(找更大元素用递增栈,找更小元素用递减栈)
- 定义栈存储内容:通常存储元素下标(便于计算距离),而非直接存储元素值
- 处理边界条件:通过虚拟边界(如首尾加0)简化边界判断
- 元素弹出逻辑:当新元素破坏单调性时,弹出栈顶元素并计算相关结果
4.2 常见问题映射表
问题类型 | 单调性 | 栈存储 | 弹出条件 | 结果计算方式 |
---|---|---|---|---|
下一个更大元素 | 递增栈 | 下标 | 新元素 > 栈顶元素 | 新元素为栈顶元素的下一个更大元素 |
柱状图最大面积 | 递增栈 | 下标 | 新元素 < 栈顶元素 | 栈顶元素为高,左右边界算宽度 |
接雨水 | 递减栈 | 下标 | 新元素 >= 栈顶元素 | 栈顶元素为中间柱,算凹槽水量 |
每日温度 | 递增栈 | 下标 | 新元素温度 > 栈顶温度 | 天数为下标差 |
五、进阶技巧与优化
5.1 双向单调栈
对于需要同时考虑左右边界的问题(如接雨水问题),可分别使用左右单调栈计算每个位置的左右最大高度,再取较小值计算水量。虽然时间复杂度同为O(n),但代码逻辑更清晰:
// 接雨水双向栈解法
public int trap(int[] height) {int n = height.length;int[] leftMax = new int[n];int[] rightMax = new int[n];// 左单调栈(递增)Stack<Integer> leftStack = new Stack<>();for (int i = 0; i < n; i++) {while (!leftStack.isEmpty() && height[leftStack.peek()] <= height[i]) {leftStack.pop();}leftMax[i] = leftStack.isEmpty() ? 0 : height[leftStack.peek()];leftStack.push(i);}// 右单调栈(递增)Stack<Integer> rightStack = new Stack<>();for (int i = n-1; i >= 0; i--) {while (!rightStack.isEmpty() && height[rightStack.peek()] <= height[i]) {rightStack.pop();}rightMax[i] = rightStack.isEmpty() ? 0 : height[rightStack.peek()];rightStack.push(i);}int res = 0;for (int i = 0; i < n; i++) {res += (Math.min(leftMax[i], rightMax[i]) - height[i]);}return res;
}
5.2 单调栈与动态规划结合
在「股票买卖的最佳时机」系列问题中,可结合单调栈优化动态规划的状态转移,将O(n²)的暴力解法优化至O(n),我将在之后的博客中进行讲解,持续关注不错过哦。
5.3 注意事项
- 下标与值的区分:栈中存储下标而非值,便于计算元素间的距离(如宽度、天数差)
- 虚拟元素处理:通过在数组首尾添加虚拟元素(如0),避免单独处理边界条件
- 单调性方向判断:明确问题需要「更大」还是「更小」元素,选择对应的单调栈类型
单调栈通过维护元素的单调性,将原本需要双重循环的序关系问题优化至线性时间,是解决「下一个更大/更小元素」类问题的核心工具。其
核心思想
是:用栈结构缓存尚未处理的元素,通过单调性约束快速确定元素间的序关系。
That’s all, thanks for reading!
觉得有用就点个赞
、收进收藏
夹吧!关注
我,获取更多干货~