【单调栈和单调队列】LeetCode hot100+面试高频

文章目录

单调栈

单调栈:只保留**"有潜力成为答案"**的数据。

一旦出现一个"矮个子",它就会把前面所有"比它高的"全部杀掉,维护一个从矮到高的优良序列。

这样每个元素最多进一次、死一次,效率 O(N)。

所以,单调栈本质上不是为了"存储",而是为了**"维护一种有序性",从而实现O(1) 时间找到最近的极值**。

模式识别:一旦题目让你在数组里找"左边/右边第一个比我大/小的元素",立刻想到单调栈。

复杂度:每个元素最多进栈一次、出栈一次,所以时间复杂度是 O(N)。

LeetCode 739.每日温度

栈中记录还没算出下一个更大元素的那些数的下标。

相当于栈是一个 todolist,在循环的过程中,现在还不知道答案是多少,在后面的循环中会算出答案。

java 复制代码
class Solution {
    
    public int[] dailyTemperatures(int[] temperatures) {
        int n = temperatures.length;
        int[] ans = new int[n];
        Deque<Integer> st = new ArrayDeque<>();
        for(int i = 0; i < n; i++){
            int t = temperatures[i];
            while(!st.isEmpty() && t > temperatures[st.peek()]){
                int j = st.pop();
                ans[j] = i - j;
            }
            st.push(i);
        }
        return ans;
    }
}

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

java 复制代码
class Solution {
    public int largestRectangleArea(int[] heights) {
       int n = heights.length;
       // left[i] 存的是:第 i 根柱子左边第一个比它矮的柱子的下标
       int[] left = new int[n];
       // right[i] 存的是:第 i 根柱子右边第一个比它矮的柱子的下标
       int[] right = new int[n];

        //维护一个从栈底到栈顶高度递增的栈(因为每次都是踢掉高的,留下矮的)
        Deque<Integer> st = new ArrayDeque<>();

        // 第一步:从左往右遍历,给每个i找左边界 (Left Less Element)
        //把比自己高和和自己一样高的踢掉
        for(int i = 0; i < n; i++){
            while(!st.isEmpty() && heights[st.peek()] >= heights[i]){
                st.pop();
            }
            // 踢完高个子后:
            // 1. 如果栈空了:说明左边没有任何柱子比我矮,我的左边界延伸到了无穷远(即下标 -1)。
            // 2. 如果栈不空:栈顶剩下的那个就是"第一个比我矮"的家伙,它就是我的左边界。
            left[i] = st.isEmpty()?-1:st.peek();

            // 把当前柱子加入栈,作为后面柱子的潜在边界
            st.push(i);
        }

        // 第二步:从右往左遍历,找右边界 (Right Less Element)
        st.clear(); // 【重要】一定要记得清空栈,复用它
        for(int i = n-1; i>=0; i--){//注意这里是--
            while(!st.isEmpty() && heights[st.peek()] >= heights[i]){
                st.pop();
            }
            // 1. 如果栈空了:说明右边没有比我矮的,边界延伸到数组最右端之外(即下标 n)。
            // 2. 如果栈不空:栈顶就是右边第一个比我矮的。
            right[i] = st.isEmpty() ? n: st.peek();
            st.push(i);
        }
        
        // 第三步:遍历每根柱子,计算以它为高度的最大矩形面积
        int ans = 0;
        for(int i = 0; i < n; i++){
            int h = heights[i];// 矩形高度:取决于当前柱子

            // 矩形宽度:右边界 - 左边界 - 1
            // 举例:左边界是下标 1,右边界是下标 5。
            // 也就是中间夹着的下标是 2, 3, 4。
            // 宽度 = 5 - 1 - 1 = 3。
            int w = right[i] - left[i] -1;
            ans = Math.max(ans,h*w);
        }

        return ans;
}

    
}

单调栈解法,维护一个从栈底到栈顶高度递增的栈(因为每次都是踢掉高的,留下矮的)。
踢掉无用数据,保持栈中有序

柱状图中最大的矩形不能直接用(接雨水那种)前后缀方法做,因为逻辑完全不同。

虽然这两道题都需要"往左看"和"往右看",也都可以用 单调栈 ,但在使用 前后缀数组 (Prefix/Suffix Array) 这个思路上,它们有本质的区别。


1. 接雨水 (Trapping Rain Water)

核心逻辑 :"我不关心旁边的人是谁,我只关心这一侧最高的那个墙有多高。"

  • 问题 :当前位置 i 能存多少水?
  • 公式Water[i] = min(左边最高的墙, 右边最高的墙) - height[i]
  • 关键点 :这是一个累积 (Cumulative) 的概念。
    • 只要左边有一个 100 米高的墙,不管它离我多远,它都能帮我挡水。
  • 前后缀解法
    • leftMax[i]:从 0 到 i 的最大值。(简单的动态规划/一次遍历)
    • rightMax[i]:从 n-1i 的最大值。
    • 不需要栈,只需要比大小。
java 复制代码
// 接雨水的前后缀解法(不需要栈)
leftMax[i] = Math.max(leftMax[i-1], height[i]); // 只关心最大的

2. 柱状图最大矩形 (Largest Rectangle)

核心逻辑 :"我非常关心旁边的人是谁,只要遇到一个比我矮的,我就完蛋了(断开了)。"

  • 问题 :以当前高度 height[i] 为矩形高度,能向两边延伸多远?
  • 关键点 :这是一个最近邻 (Nearest Neighbor) 的概念。
    • 哪怕左边远方有一个 100 米高的墙,只要我紧挨着的左边是个 1 米的矮子,我就伸不过去,远处的 100 米对我毫无意义。
  • 为什么前后缀(累积最大/最小)不管用?
    • 如果你存"左边最大的",没用,挡不住我。
    • 如果你存"左边最小的",也没用,那是最矮的板,但我可能被中间某个"次矮"的挡住。
  • 解法 :必须找到离我最近的、比我小的 那个下标。
    • 这就是 单调栈 的定义:专门用来找 Next/Previous Smaller/Greater Element

3. 图解对比

假设数组:[5, 6, 2, 6, 5],我们看中间的 2

接雨水视角(找靠山):

  • 左边最高是 6 ,右边最高是 6
  • 所以我(2)上面能存 min(6, 6) - 2 = 4 的水。
  • 逻辑:找极值(Max/Min)。

柱状图视角(找边界):

  • 想以 2 为高度做矩形。
  • 左边是 6 (比我高,通过) -> 5 (比我高,通过) -> 边界。
  • 右边是 6 (比我高,通过) -> 5 (比我高,通过) -> 边界。
  • 逻辑 :找最近的坏人(第一个比我小的)。

  1. 总结:什么时候用什么?
特性 接雨水 (Rain Water) 柱状图 (Histogram)
关注点 这一侧最高的墙 (Cumulative Max) 离我最近的比我矮的墙 (Nearest Smaller)
受阻条件 只有比当前水位更高的墙才重要 只要比我矮,马上截断
前后缀数组 可以用 (简单遍历取Max) 不能直接用 (逻辑不支持)
单调栈 可以用 (找凹槽) 必须用 (找左右边界)

你的"错觉"来源

你刚才写的那个代码里,确实定义了 left[]right[] 两个数组。

但这不是 传统意义上的"前后缀数组"(像接雨水那样 max(prev, curr) 算出来的)。

这两个数组是依靠单调栈算出来的"索引数组"。

结论

  • 如果面试问"这题能用前后缀做吗?" -> 回答:不能,必须用单调栈。
  • 如果面试问"接雨水能用前后缀做吗?" -> 回答:可以,那是接雨水的标准解法之一(双指针其实就是前后缀的空间优化版)。
相关推荐
程序员-King.1 小时前
day109—同向双指针(字符串)—每个字符最多出现两次的最长子字符串(LeetCode-3090)
算法·leetcode·双指针
俊俊谢1 小时前
【浮点运算性能优化】浮点转定点算法库的多平台通用移植方案与性能评估优化
算法·性能优化·c·浮点转定点·多平台移植
电饭叔1 小时前
Luhn算法与信用卡识别完善《python语言程序设计》2018版--第8章14题利用字符串输入作为一个信用卡号之三
android·python·算法
bbq粉刷匠1 小时前
力扣-电话号码组合
java·算法
狗头实习生1 小时前
电话号码字母组合
java·算法·leetcode
C雨后彩虹1 小时前
矩阵扩散问题
java·数据结构·算法·华为·面试
独自破碎E1 小时前
力场重叠问题
java·开发语言·算法
free-elcmacom1 小时前
机器学习入门<5>支持向量机形象教学:寻找最安全的“三八线”,人人都能懂的算法核心
人工智能·python·算法·机器学习·支持向量机