高阶算法技巧:单调栈与单调队列详解
一、核心思想
单调栈 和单调队列 通过维护一个元素按特定顺序(递增或递减)排列的结构,将某些问题的复杂度从 O(n²) 优化到 O(n)。核心在于及时排除无效元素,减少不必要的计算。
二、单调队列案例:滑动窗口最大值
问题描述 :给定数组和窗口大小 k,求每个窗口的最大值。
暴力法问题:遍历每个窗口找最大值,时间复杂度 O(nk)。
优化思路:维护一个单调递减队列,队头为当前窗口最大值。
步骤分解:
- 队列中存储元素索引,保证对应值递减。
- 新元素入队前,移除队尾比它小的元素。
- 检查队头是否在窗口内,否则移除。
- 当窗口形成后(i ≥ k-1),记录队头值。
图解:
ini
数组:[1,3,-1,-3,5,3,6,7], k=3
窗口位置 队列内容(索引) 最大值
[1 3 -1] [1,2](值3,-1) 3
1 [3 -1 -3] [1,2,3](3,-1,-3) 3
1 3 [-1 -3 5] [4](5) 5
...
代码示例:
java
import java.util.*;
public class SlidingWindowMax {
public static int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> q = new ArrayDeque<>();
List<Integer> result = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
// 维护单调递减性
while (!q.isEmpty() && nums[q.peekLast()] <= nums[i]) {
q.pollLast();
}
q.offerLast(i);
// 移除越界的队首元素
if (q.peekFirst() == i - k) {
q.pollFirst();
}
// 窗口形成后记录结果
if (i >= k - 1) {
result.add(nums[q.peekFirst()]);
}
}
return result.stream().mapToInt(Integer::intValue).toArray();
}
public static void main(String[] args) {
int[] nums = {1,3,-1,-3,5,3,6,7};
System.out.println(Arrays.toString(maxSlidingWindow(nums, 3)));
// 输出: [3,3,5,5,6,7]
}
}
三、单调栈案例1:接雨水
问题描述:计算柱子排列后能接的雨水总量。
优化思路:维护单调递减栈,遇到较高柱子时计算积水。
步骤分解:
- 栈保存索引,对应高度递减。
- 当前高度 > 栈顶高度时,弹出栈顶作为坑底。
- 新栈顶为左边界,计算宽度和高度差。
图解:
scss
高度:[0,1,0,2,1,0,1,3,2,1,2,1]
处理索引2(高度0)时,栈为[1(1)] → 无积水。
处理索引3(高度2)时,弹出0(0),左边界1(1),积水宽度3-1-1=1,高度min(2,1)-0=1 → 积水量1×1=1。
代码示例:
java
import java.util.*;
public class TrappingRainWater {
public static int trap(int[] height) {
Stack<Integer> stack = new Stack<>();
int water = 0;
for (int i = 0; i < height.length; i++) {
while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
int bottom = stack.pop();
if (stack.isEmpty()) break;
int left = stack.peek();
int width = i - left - 1;
int depth = Math.min(height[i], height[left]) - height[bottom];
water += width * depth;
}
stack.push(i);
}
return water;
}
public static void main(String[] args) {
int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
System.out.println(trap(height)); // 输出: 6
}
}
四、单调栈案例2:柱状图中的最大矩形
问题描述:找到柱状图中最大的矩形面积。
优化思路:对每个柱子,找到左右第一个比它矮的边界。
步骤分解:
- 维护递增栈,存储(索引,高度)。
- 当前高度 < 栈顶高度时,确定栈顶的右边界。
- 左边界为栈中前一个元素,计算面积。
图解:
scss
高度:[2,1,5,6,2,3]
处理索引4(高度2)时,弹出6(3)→左边界2(5),面积6*(4-2-1)=6。
继续弹出5(2)→左边界-1,面积5*4=20。
代码示例:
java
import java.util.*;
public class LargestRectangle {
public static int largestRectangleArea(int[] heights) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(-1); // 哨兵
int maxArea = 0;
int[] extended = Arrays.copyOf(heights, heights.length + 1);
extended[heights.length] = 0; // 触发最终计算
for (int i = 0; i < extended.length; i++) {
while (stack.peek() != -1 && extended[i] < extended[stack.peek()]) {
int h = extended[stack.pop()];
int w = i - stack.peek() - 1;
maxArea = Math.max(maxArea, h * w);
}
stack.push(i);
}
return maxArea;
}
public static void main(String[] args) {
int[] heights = {2,1,5,6,2,3};
System.out.println(largestRectangleArea(heights)); // 输出: 10
}
}
五、总结与适用场景
优势:
- 时间复杂度从 O(n²) → O(n)
- 空间复杂度 O(n)
适用场景:
- 滑动窗口最值 → 单调队列
- 边界查找问题(雨水、矩形)→ 单调栈
- 其他变种:下一个更大元素、股票跨度等
核心思维导图:
单调结构 → 维护有序性
├─ 队列:动态窗口,淘汰旧元素
└─ 栈:历史数据,找到最近更大/小元素
掌握单调栈/队列后,可高效解决一系列看似复杂的问题,关键在于识别问题中的"无效元素"并及时剔除,聚焦核心计算。