栈(Stack)是一种后进先出(LIFO)的线性数据结构,其操作仅限于在栈顶进行插入(push)和删除(pop)。虽然栈的实现极为简单,但基于栈的扩展应用却非常丰富。本文将从零实现一个动态扩容栈,并重点讲解单调栈这一高级技巧——它能够在 O(n) 时间内解决一类”寻找下一个更大/更小元素”的经典问题,以及直方图中最大矩形这类高频面试题。
数据结构原理
栈支持两种核心操作:push(压栈)将元素放到栈顶,pop(弹栈)移除并返回栈顶元素。由于只操作栈顶,所有操作均为 O(1)。
单调栈是栈的进阶应用,其核心思想是维护栈内元素的单调性(递增或递减)。当新元素破坏单调性时,持续弹出栈顶元素并处理,从而在一次遍历中完成复杂的状态转移。
Java 实现
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
/**
* 栈的完整实现与单调栈高级应用
*/
public class StackDemo {
/**
* 基于数组的动态扩容栈
*/
static class ArrayStack {
private int[] data;
private int size;
private static final int DEFAULT_CAPACITY = 10;
public ArrayStack() {
data = new int[DEFAULT_CAPACITY];
size = 0;
}
/**
* 压栈操作
*/
public void push(int val) {
ensureCapacity();
data[size++] = val;
}
/**
* 弹栈操作
*
* @return 栈顶元素
*/
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return data[--size];
}
/**
* 查看栈顶元素但不移除
*/
public int peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return data[size - 1];
}
public boolean isEmpty() {
return size == 0;
}
public int size() {
return size;
}
private void ensureCapacity() {
if (size == data.length) {
data = Arrays.copyOf(data, data.length * 2); // 容量翻倍
}
}
}
/**
* 单调栈应用一:寻找每个元素的下一个更大元素
* 给定数组,返回每个元素右边第一个比它大的元素,不存在则为 -1
*
* @param nums 输入数组
* @return 下一个更大元素数组
*/
public static int[] nextGreaterElement(int[] nums) {
int n = nums.length;
int[] result = new int[n];
Arrays.fill(result, -1); // 默认无更大元素
Deque<Integer> stack = new ArrayDeque<>(); // 存放下标,保持栈内元素值递减
for (int i = 0; i < n; i++) {
// 当当前元素大于栈顶元素时,栈顶元素的下一个更大元素就是当前元素
while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
int idx = stack.pop();
result[idx] = nums[i];
}
stack.push(i);
}
// 栈中剩余元素无下一个更大元素,保持 -1
return result;
}
/**
* 单调栈应用二:寻找每个元素的左边第一个更小元素的下标
*
* @param heights 输入数组
* @return 左边第一个更小元素的下标数组
*/
public static int[] leftSmallerIndex(int[] heights) {
int n = heights.length;
int[] left = new int[n];
Deque<Integer> stack = new ArrayDeque<>(); // 单调递增栈
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
stack.pop();
}
left[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
return left;
}
/**
* 单调栈应用三:直方图中最大的矩形
* 给定 n 个非负整数表示柱状图高度,求能勾勒出的最大矩形面积
*
* 核心思路:对每个柱子,找左右两边第一个比它矮的柱子,
* 则该柱子为高的最大矩形宽度即为两边界距离
*
* @param heights 柱状图高度数组
* @return 最大矩形面积
*/
public static int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] left = new int[n]; // 左边第一个更小元素的下标
int[] right = new int[n]; // 右边第一个更小元素的下标
Deque<Integer> stack = new ArrayDeque<>();
// 计算左边界
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
stack.pop();
}
left[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
stack.clear();
// 计算右边界
for (int i = n - 1; i >= 0; i--) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
stack.pop();
}
right[i] = stack.isEmpty() ? n : stack.peek();
stack.push(i);
}
// 计算最大面积
int maxArea = 0;
for (int i = 0; i < n; i++) {
int width = right[i] - left[i] - 1; // 有效宽度
int area = heights[i] * width;
maxArea = Math.max(maxArea, area);
}
return maxArea;
}
/**
* 优化版:一次遍历计算最大矩形面积
* 在数组末尾添加高度为 0 的哨兵,确保所有元素都会被弹出处理
*/
public static int largestRectangleAreaOptimized(int[] heights) {
int n = heights.length;
int[] h = Arrays.copyOf(heights, n + 1);
h[n] = 0; // 哨兵
Deque<Integer> stack = new ArrayDeque<>();
int maxArea = 0;
for (int i = 0; i <= n; i++) {
while (!stack.isEmpty() && h[stack.peek()] > h[i]) {
int height = h[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}
public static void main(String[] args) {
// 测试基础栈
ArrayStack stack = new ArrayStack();
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("栈顶元素: " + stack.peek());
System.out.println("弹栈: " + stack.pop());
System.out.println("当前大小: " + stack.size());
// 测试下一个更大元素
int[] nums = {2, 1, 2, 4, 3};
System.out.println("原始数组: " + Arrays.toString(nums));
System.out.println("下一个更大元素: " + Arrays.toString(nextGreaterElement(nums)));
// 测试最大矩形
int[] heights = {2, 1, 5, 6, 2, 3};
System.out.println("柱状图高度: " + Arrays.toString(heights));
System.out.println("最大矩形面积: " + largestRectangleArea(heights));
System.out.println("优化版最大矩形面积: " + largestRectangleAreaOptimized(heights));
}
}
复杂度分析
| 操作/算法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| push / pop | O(1) 均摊 | O(1) |
| 下一个更大元素 | O(n) | O(n) |
| 直方图最大矩形(两次遍历) | O(n) | O(n) |
| 直方图最大矩形(优化版) | O(n) | O(n) |
单调栈将原本需要 O(n²) 的暴力枚举优化到 O(n),其本质是利用栈维护了”尚未找到答案的元素”,在遍历过程中动态消除已解决的子问题。
适用场景
单调栈适用于以下场景:寻找下一个更大/更小元素;直方图最大矩形;接雨水(双指针也可用);股票跨度问题;以及需要维护元素单调性的滑动窗口问题。掌握单调栈思想,是进阶算法能力的标志之一。