图解:单调栈算法模板(Java语言)

一、单调栈是什么?用排队来理解

先看一个生活场景:

复制代码
你站在一排人中,从前往后看,只能看到比你矮的人。

  身高:6  3  1  4  5
  位置:0  1  2  3  4

从右往左看(站在最右边往左看):
  站在位置4,身高5 → 往左看,能看到 1, 3, 4
  但看不到 6(被5挡住了)

单调栈就是维护这样一个"从某个方向看,高度递增/递减"的结构。

核心思想:

遍历数组时,维护一个栈,栈中的元素始终保持单调递增(或递减)。当新元素不满足单调性时,弹出栈顶元素,处理它与新元素的关系。


二、核心原理

2.1 为什么叫"单调"?

栈中元素始终保持单调递增单调递减

复制代码
单调递增栈(从栈底到栈顶递增):
  栈底 → [1, 3, 5, 7] ← 栈顶
  新元素 4 来了 → 7 和 5 比 4 大,弹出 7 和 5
  栈变成 → [1, 3, 4] ← 满足单调递增 ✅

单调递减栈(从栈底到栈顶递减):
  栈底 → [7, 5, 3, 1] ← 栈顶
  新元素 4 来了 → 1 比 4 小,弹出 1
  栈变成 → [7, 5, 3, 4] ← 满足单调递减 ✅

2.2 求什么决定了用哪种栈

复制代码
┌───────────────────────────────────────────────────────────┐
│  问题类型              │  栈的类型      │  弹出时的含义        │
├────────────────────────┼────────────────┼───────────────────┤
│  找下一个更大元素        │  单调递减栈     │  弹出=找到了更大值    │
│  找下一个更小元素        │  单调递增栈     │  弹出=找到了更小值    │
│  找左边第一个更大/小     │  从左往右遍历    │  弹出=当前元素是答案  │
│  找右边第一个更大/小     │  从右往左遍历    │  弹出=当前元素是答案  │
└────────────────────────┴────────────────┴───────────────────┘

三、通用模板代码

java 复制代码
/**
 * 单调栈模板:找每个元素右边第一个更大的元素
 * 如果要找更小的,把 while 里的 > 改成 <
 */
public 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[i] > nums[stack.peek()]) {
            int idx = stack.pop();     // 弹出栈顶
            result[idx] = nums[i];     // 当前元素就是栈顶的下一个更大元素
        }
        stack.push(i); // 当前元素入栈
    }

    return result;
}

四、经典题1:下一个更大元素I

LeetCode 496 --- 下一个更大元素 I

给定 nums1 和 nums2,对于 nums1 中的每个元素,找出在 nums2 中对应位置的下一个更大元素。

4.1 思路

经典的"下一个更大元素"问题。用单调递减栈,从左到右遍历 nums2。

4.2 图解过程

nums2 = [2, 1, 2, 4, 3], nums1 = [2, 4]

复制代码
用 HashMap 先记录 nums2 中每个元素的「下一个更大值」
最后根据 nums1 查 HashMap

遍历 nums2:

i=0, nums2[0]=2
  栈: [2]  (存下标)

i=1, nums2[1]=1
  1 < 2,不弹栈
  栈: [2, 1]

i=2, nums2[2]=2
  2 > 1 → 弹出1,result[1]=2  ← 元素1的下一个更大是2
  2 不大于 2,停止
  栈: [2, 2]

i=3, nums2[3]=4
  4 > 2 → 弹出2(idx=2),result[2]=4
  4 > 2 → 弹出2(idx=0),result[0]=4
  栈: [4]

i=4, nums2[4]=3
  3 < 4,不弹栈
  栈: [4, 3]

最终 map: {2→4, 1→2}
nums1[0]=2 → 4
nums1[1]=4 → -1(没有更大元素)

4.3 完整代码

java 复制代码
class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        // 第一步:遍历 nums2,用单调栈找出每个元素的下一个更大值
        Map<Integer, Integer> nextGreater = new HashMap<>();
        Deque<Integer> stack = new ArrayDeque<>();

        for (int num : nums2) {
            while (!stack.isEmpty() && num > stack.peek()) {
                nextGreater.put(stack.pop(), num);
            }
            stack.push(num);
        }

        // 剩余栈中元素没有下一个更大值(默认-1)

        // 第二步:根据 nums1 查询
        int[] result = new int[nums1.length];
        for (int i = 0; i < nums1.length; i++) {
            result[i] = nextGreater.getOrDefault(nums1[i], -1);
        }
        return result;
    }
}

五、经典题2:柱状图中最大矩形

LeetCode 84 --- 柱状图中最大矩形

给定 n 个非负整数表示柱状图中各个柱子的高度,求最大矩形面积。

5.1 思路

关键思路:对于每根柱子,找到它能向左右延伸的最大宽度(即找到左边和右边第一个比它矮的柱子)。

这正好是"找下一个更小元素",用单调递增栈

5.2 图解过程

heights = [2, 1, 5, 6, 2, 3]

复制代码
柱状图:
        ┌──┐
    ┌──┤  │
  ┌─┤  │  │  ┌──┐
  │ │  │  │  │  │
┌─┤ │  │  │  │  │
│ │ │  │  │  │  │
2 1 5  6  2  3

关键:每根柱子的矩形面积 = 高度 × 能延伸的最大宽度

柱子0(高2): 左边界=-1, 右边界=1 → 宽=1-(-1)-1=1 → 面积=2×1=2
柱子1(高1): 左边界=-1, 右边界=6 → 宽=6-(-1)-1=6 → 面积=1×6=6
柱子2(高5): 左边界=1, 右边界=4 → 宽=4-1-1=2 → 面积=5×2=10
柱子3(高6): 左边界=2, 右边界=4 → 宽=4-2-1=1 → 面积=6×1=6
柱子4(高2): 左边界=1, 右边界=6 → 宽=6-1-1=4 → 面积=2×4=8
柱子5(高3): 左边界=4, 右边界=6 → 宽=6-4-1=1 → 面积=3×1=3

最大面积 = 10(柱子2)

5.3 完整代码

java 复制代码
class Solution {
    public int largestRectangleArea(int[] heights) {
        int n = heights.length;
        int maxArea = 0;

        // 单调递增栈,存下标
        Deque<Integer> stack = new ArrayDeque<>();

        // 在数组前后各加一个高度为0的哨兵,保证所有柱子都能被弹出
        int[] newHeights = new int[n + 2];
        System.arraycopy(heights, 0, newHeights, 1, n);
        // newHeights[0] 和 newHeights[n+1] 默认为0

        for (int i = 0; i < newHeights.length; i++) {
            // 当前柱子比栈顶矮 → 栈顶找到了右边界
            while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {
                int height = newHeights[stack.pop()]; // 弹出的柱子高度
                int left = stack.peek();               // 左边界(栈中上一个元素)
                int right = i;                         // 右边界(当前元素)
                int width = right - left - 1;          // 宽度
                maxArea = Math.max(maxArea, height * width);
            }
            stack.push(i);
        }

        return maxArea;
    }
}

六、经典题3:接雨水(单调栈解法)

LeetCode 42 --- 接雨水

给定 n 个非负整数表示柱子高度,计算能接多少雨水。

6.1 与双指针解法对比

之前的文章用双指针解过这道题。双指针是按列计算 ,单调栈是按行计算

复制代码
双指针:看每根柱子上方能装多少水(纵向计算)
单调栈:看每层能装多少水(横向计算)

      ↓ 这一层的水
    ┌─────┐
    │ 水  │
┌───┤ 水  ├───┐
│   │ 水  │   │
│   └─────┘   │
└───────────────┘

6.2 单调栈思路

单调递减栈存储柱子下标。当遇到比栈顶高的柱子时:

  1. 弹出栈顶(凹槽底部)
  2. 新的栈顶是左边界
  3. 当前元素是右边界
  4. 水的宽度 = right - left - 1
  5. 水的高度 = min(heightsleft, heightsright) - heights凹槽

6.3 图解

height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]

复制代码
              |
    |         | |   |
    |   |     | | | |
  __|_|_|_|___|_|_|_|__

关键过程:
当 i=3, height=2 时:
  弹出 i=2(高0), 左边界=1(高1), 右边界=3(高2)
  水 = min(1,2) - 0 = 1, 宽 = 3-1-1 = 1
  接水 = 1 × 1 = 1

当 i=7, height=3 时:
  弹出 i=6(高1), 左边界=5(高0)→继续弹出
  弹出 i=5(高0), 左边界=3(高2), 右边界=7(高3)
  水 = min(2,3) - 0 = 2, 宽 = 7-3-1 = 3
  接水 = 2 × 3 = 6

6.4 完整代码

java 复制代码
class Solution {
    public int trap(int[] height) {
        int totalWater = 0;
        Deque<Integer> stack = new ArrayDeque<>();

        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 right = i;             // 右边界

                int waterHeight = Math.min(height[left], height[right]) - height[bottom];
                int waterWidth = right - left - 1;

                totalWater += waterHeight * waterWidth;
            }
            stack.push(i);
        }

        return totalWater;
    }
}

6.5 两种解法对比

对比项 双指针解法 单调栈解法
思路 按列纵向算 按行横向算
时间 O(n) O(n)
空间 O(1) O(n)
理解难度 ⭐⭐⭐ ⭐⭐⭐⭐
适用性 简单场景 更通用

七、单调栈 vs 单调队列

复制代码
┌──────────────────────────────────────────────────────┐
│  单调栈 vs 单调队列                                    │
├───────────┬────────────────┬─────────────────────────┤
│  对比项    │    单调栈       │     单调队列              │
├───────────┼────────────────┼─────────────────────────┤
│  数据结构  │  Stack(后进先出)│  Deque(先进先出)       │
│  典型场景  │  找下一个更大/小   │  滑动窗口最大/最小值     │
│  遍历方向  │  通常单向         │  窗口两端               │
│  代表题目  │  LeetCode 84/42  │  LeetCode 239           │
│  核心操作  │  弹出不满足条件的  │  弹出过期的 + 不满足的    │
│  Java实现  │  ArrayDeque      │  ArrayDeque             │
└───────────┴────────────────┴─────────────────────────┘

💡 总结 :单调栈的核心就一句话------维护一个有序的结构,弹出的那一刻就是找到答案的时刻。记住模板,找到"找下一个XX"的关键词,直接套。

如果觉得有帮助,点赞 + 收藏支持一下!有问题欢迎评论区讨论 💬

相关推荐
IronMurphy1 小时前
多线程问!
java·jvm·spring
小灰灰搞电子1 小时前
C++ boost::circular_buffer 详解:原理、用法与实战
开发语言·c++·boost
vx-Biye_Design1 小时前
springboot安阳地区研学旅游服务小程序-计算机毕业设计源码12785
java·vue.js·windows·spring boot·tomcat·maven·mybatis
whaledown1 小时前
Kafka 与 Java 消息队列入门:用订单场景理解核心机制
java·kafka·消息队列·springboot
Moshow郑锴2 小时前
Ubuntu用SDKMAN轻松管理多个Java 版本
java·ubuntu·sdkman
生成论实验室2 小时前
自动驾驶:一个自主运动的系统
人工智能·算法·机器学习·语言模型·机器人·自动驾驶·安全架构
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2026.06.16 题目:3612. 字符串特殊符号处理
笔记·算法·leetcode
阿昌喜欢吃黄桃2 小时前
RocketMq事务消息原理
java·中间件·消息队列·rocketmq·mq
CoderYanger2 小时前
A.每日一题:2095. 删除链表的中间节点
java·数据结构·程序人生·leetcode·链表·面试·职场和发展