算法实战笔记:空间换时间的黑魔法——单调栈全景解析(十一)

算法实战笔记:空间换时间的黑魔法------单调栈全景解析(十一)

在算法刷题的过程中,有一类问题常常让人觉得十分"憋屈":寻找数组中任一元素右边(或左边)第一个比自己大(或小)的元素。

如果用最直观的暴力解法,我们需要两层嵌套循环:对于每一个元素,都往后遍历寻找符合条件的目标,时间复杂度直接飙升至 O(n2)O(n^2)O(n2)。在数据量稍大的情况下,必然会超时。

这个时候,我们就需要请出能化腐朽为神奇的黑魔法结构------单调栈(Monotonic Stack)。它能够利用"空间换时间"的极致思想,将整个寻找过程的时间复杂度硬生生压到 O(n)。

一、 单调栈的底层哲学

单调栈的本质,其实是用一个栈来记录我们遍历过的元素

为什么需要记录?因为在遍历数组时,对于那些"目前还没找到更大(或更小)元素"的数字,我们不能就把它们忘了,我们需要一个"候车室"把它们暂存起来。单调栈就是这样一个特殊的候车室,它巧妙地利用了入栈和出栈的时机,保持了栈内元素的单调性。

在使用单调栈之前,你必须在脑海里清晰地回答两个问题:

1. 栈里到底存什么?

单调栈里存放的永远是元素的"下标(索引)",而不是具体的数值!

这是初学者最容易踩的坑。因为我们在计算诸如"接雨水的宽度"或"等待的天数"时,必须使用下标相减。如果你想获取数值,只要直接用 nums[i] 就能瞬间拿到,所以存下标是一举两得的选择。

2. 栈内是递增还是递减?

这里的递增递减,我们统一约定指从栈头(栈顶)到栈底的顺序:

  • 求右边第一个更大的元素 :单调栈是递增的(栈顶最小,栈底最大)。因为只有当新来的元素比栈顶大时,它才正是栈顶元素苦苦寻找的那个"更大值",此时触发弹栈记录结果;否则就乖乖入栈等待。
  • 求右边第一个更小的元素 :单调栈则是递减的(栈顶最大,栈底最小)。

二、 破冰模板:每日温度 (LeetCode 739)

这是单调栈的教科书级入门题。题目要求:根据每日气温列表,计算出还需要等待几天才能遇到一个更高的气温。

这就是典型的"求右边第一个更大元素"。我们维护一个栈顶到栈底递增的单调栈:

java 复制代码
class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int lens = temperatures.length;
        int[] res = new int[lens];
        
        // 单调栈:存放下标,栈顶到栈底元素对应的值递增
        Deque<Integer> stack = new LinkedList<>();
        
        for(int i = 0; i < lens; i++) {
            // 如果栈不为空,且当前遍历的元素 大于 栈顶元素对应的值
            // 说明找到了栈顶元素右侧的第一个更大值!
            while(!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
                // 记录答案:等待的天数 = 当前下标 - 栈顶下标
                res[stack.peek()] = i - stack.peek();
                // 栈顶元素使命完成,弹出
                stack.pop();
            }
            // 当前元素下标入栈,等待属于它的那个"更大值"
            stack.push(i);
        }
        return res;
    }
}

三、 进阶双煞:接雨水与最大矩形

当你吃透了每日温度的模板,单调栈的真正威力才刚刚展现。在面试中,真正让你拉开差距的是接雨水和最大矩形这两道 Hard 题。

1. 接雨水 (LeetCode 42)

核心思路:找"凹槽"

要接住雨水,必须要有凹槽。凹槽是怎么形成的?就是当前元素比左边高,同时也比右边高!

因此,我们要找的是右边第一个比自己高的元素 ,单调栈从栈顶到栈底自然是递增的。

height[i] > height[stack.peek()] 时,说明坑的右边界找到了!此时弹出的栈顶元素就是"坑底",而弹出坑底后,新的栈顶元素就是坑的"左边界"。

复制代码
高度 h = Math.min(左右边界高度) - 坑底高度
宽度 w = 右边界下标 i - 左边界下标 - 1
java 复制代码
class Solution {
    public int trap(int[] height) {
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(0);
        int res = 0;
        
        for (int i = 1; i < height.length; i++) {
            // 发现右侧比栈顶高,可能形成凹槽
            while (!stack.isEmpty() && height[stack.peek()] < height[i]) {
                int mid = stack.pop(); // 凹槽底部
                if (!stack.isEmpty()) {
                    // 凹槽左侧边界就是新的栈顶
                    int h = Math.min(height[stack.peek()], height[i]) - height[mid];
                    int w = i - stack.peek() - 1;
                    res += h * w;
                }
            }
            stack.push(i);
        }
        return res;
    }
}

2. 柱状图中最大的矩形 (LeetCode 84)

核心思路:找"凸起"

要找到以某个柱子为高度的最大矩形,我们必须知道这根柱子能向左、向右延伸多远。延伸的条件是:左右两边的柱子不能比自己矮。

所以,我们要找的是左右两侧第一个比自己矮的柱子 。既然找更小元素,单调栈从栈顶到栈底就必须是递减的!

神仙细节:首尾加 0

如果原数组一直是单调递增的,栈里的元素将永远不会被弹出结算。为了强行触发结算,我们需要在原数组的头部和尾部分别加上一个高度为 0 的哨兵元素。

java 复制代码
class Solution {
    public int largestRectangleArea(int[] heights) {
        // 首尾加哨兵 0,保证所有元素都能被完美弹栈结算
        int[] newHeights = new int[heights.length + 2];
        System.arraycopy(heights, 0, newHeights, 1, heights.length);
        heights = newHeights;
        
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(0);
        int res = 0;
        
        for (int i = 1; i < heights.length; i++) {
            // 发现右侧比栈顶矮,栈顶柱子找到了它的右边界
            while (!stack.isEmpty() && heights[stack.peek()] > heights[i]) {
                int mid = stack.pop(); // 以该柱子高度作为基准
                int h = heights[mid];
                int w = i - stack.peek() - 1; // 新栈顶就是它的左边界
                res = Math.max(res, h * w); 
            }
            stack.push(i);
        }
        return res;
    }
}

总结

单调栈并不是什么复杂的算法,而是一种极其巧妙的数据维护策略。

  • 遇到找更大 值,维护栈顶到栈底递增栈。
  • 遇到找更小 值,维护栈顶到栈底递减栈。
  • 永远铭记:栈内存下标,计算靠相减

打通了这条逻辑线,无论是算温度、接雨水还是画矩形,你都能一眼看透它们寻找"临界点"的底层本质。

相关推荐
大模型最新论文1 小时前
小红书提出 RedKnot:分头处理 kv 缓存,延时降低 60%效果还提升
算法
问心无愧05131 小时前
ctf show web入门157 158
前端·笔记
AI玫瑰助手1 小时前
Python函数:函数的文档字符串(docstring)编写
android·java·python
随意起个昵称1 小时前
线性dp-LIS题目6(友好城市,二分优化)
算法·动态规划
周末也要写八哥1 小时前
线程的生命周期之“守护“线程
java·开发语言·jvm
乐之者v1 小时前
地图技术后端开发的知识点
java
闪闪发亮的小星星1 小时前
STK-03-通信卫星方向最常遇到的场景
笔记
数据科学小丫1 小时前
算法:随机森林算法
算法·随机森林·机器学习
亦暖筑序1 小时前
Java 8老系统AI工具接入:API包装成受控工具,只读优先+权限拦截
java·人工智能·aigc·企业架构·mcp协议