【Hot100】5.栈/队列

这一类的核心思想是:利用栈的“后进先出”特性,维护一个有序序列(单调栈)或处理嵌套结构

  • 单调栈:用于解决 “下一个更大/更小元素” (Next Greater/Smaller Element) 问题。通过维护一个单调递增或递减的栈,可以在 $O(N)$ 时间内找到每个元素左右两侧第一个比它大/小的元素。
  • 辅助栈:用于处理括号匹配、表达式计算或需要回溯的场景。
  • 单调队列:专门解决 滑动窗口最值 问题(如 239 题),队头始终是当前窗口的最值。

📝 本类题型总结

题目 栈/队列类型 关键差异点 (vs 每日温度)
739. 每日温度 单调递减栈 基准:找下一个更大,存索引,弹栈时计算距离。
84. 最大矩形 单调递增栈 找左右第一个更小的。弹栈时计算 h * (右 - 左 - 1)需补 0
42. 接雨水 单调递减栈 找凹槽。弹栈底后,若栈不空,计算 min(左, 右) - 底 的水量。
20. 有效括号 普通辅助栈 不单调。左入右配,不匹配即 False,最后判空。
155. 最小栈 双栈同步 一个存数据,一个存当前最小值。Push/Pop 需同步。
239. 滑动窗口 单调递减队列 双端队列。队头存最大值。需处理窗口滑出(队头过期)逻辑。

🏆 母题:LeetCode 739. 每日温度 (Daily Temperatures)

题目内容:给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
核心逻辑:维护一个单调递减栈(栈底到栈顶温度递减),栈中存索引

  1. 遍历温度,若当前温度 T[i] > 栈顶索引对应的温度 T[stack.peek()]
    • 说明找到了栈顶元素的“下一个更高温度”。
    • 弹出栈顶 idx,计算天数 i - idx,填入结果。
    • 重复此过程直到栈空或当前温度不大于栈顶。
  2. 将当前索引 i 入栈。
import java.util.ArrayDeque;
import java.util.Deque;

class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int n = temperatures.length;
        int[] res = new int[n];
        // ⚠️ [核心数据结构] 栈中存储的是【索引】,而非温度值
        Deque<Integer> stack = new ArrayDeque<>();
        
        for (int i = 0; i < n; i++) {
            // ⚠️ [核心逻辑] 维护单调递减栈
            // 当当前温度 > 栈顶索引对应的温度时,说明找到了栈顶元素的“下一个更高温度”
            while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
                int idx = stack.pop();
                res[idx] = i - idx; // 计算天数差
            }
            stack.push(i);
        }
        
        // 栈中剩余的元素表示后面没有更高的温度,res 默认为 0,无需处理
        return res;
    }
}

🔄 变体 1:LeetCode 84. 柱状图中最大的矩形 (Largest Rectangle in Histogram)

题目内容:给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。
相比母题的变化

  1. 栈的类型:母题是单调递减;本题需维护单调递增栈
    • 目的:找到每个柱子左边和右边第一个比它矮的柱子,从而确定以该柱子为高度的最大宽度。
  2. 计算时机:当遇到一个比栈顶矮的柱子 h[i] 时,栈顶元素 top 的右边界确定了(即 i),左边界是新的栈顶 stack.peek()
    • 宽度 w = i - stack.peek() - 1
    • 面积 area = heights[top] * w
  3. 边界处理:为了处理最后栈中剩余的元素,通常在数组首尾各补一个 0
    • 头部补 0:避免栈空判断。
    • 尾部补 0:强制弹出所有剩余元素进行计算。
import java.util.ArrayDeque;
import java.util.Deque;

class Solution {
    public int largestRectangleArea(int[] heights) {
        // ⚠️ [变化点 1] 构造新数组,首尾补 0
        int[] newHeights = new int[heights.length + 2];
        System.arraycopy(heights, 0, newHeights, 1, heights.length);
        // newHeights[0] = 0; // 默认就是 0
        // newHeights[newHeights.length - 1] = 0; 
        
        Deque<Integer> stack = new ArrayDeque<>();
        int maxArea = 0;
        
        for (int i = 0; i < newHeights.length; i++) {
            // ⚠️ [变化点 2] 维护单调递增栈
            // 当当前高度 < 栈顶高度时,说明栈顶元素的右边界确定了
            while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {
                int h = newHeights[stack.pop()];
                // ⚠️ [变化点 3] 计算宽度
                // 右边界是 i (第一个比 h 小的),左边界是 stack.peek() (弹出后新的栈顶,也是比 h 小的)
                // 宽度 = 右边界 - 左边界 - 1
                int w = i - stack.peek() - 1;
                maxArea = Math.max(maxArea, h * w);
            }
            stack.push(i);
        }
        
        return maxArea;
    }
}

🔄 变体 2:LeetCode 42. 接雨水 (Trapping Rain Water) - 单调栈解法

题目内容:同前(双指针解法已讲)。这里展示单调栈解法。
相比母题的变化

  1. 栈的类型:维护单调递减栈(存索引)。
    • 目的:寻找“凹槽”。栈底到栈顶递减,说明在积累“左壁”。
  2. 计算逻辑
    • 当遇到一个比栈顶高的柱子 h[i] 时,说明形成了一个凹槽。
    • 弹出栈顶 bottom(凹槽底部)。
    • 如果栈不空,新的栈顶 left 是左壁,当前 i 是右壁。
    • 高度 H = min(h[left], h[i]) - h[bottom]
    • 宽度 W = i - left - 1
    • 累加水量 H * W
  3. 区别:双指针是按列算水(关注左右最大值),单调栈是按层/按凹槽算水(关注局部低洼)。
import java.util.ArrayDeque;
import java.util.Deque;

class Solution {
    public int trap(int[] height) {
        Deque<Integer> stack = new ArrayDeque<>();
        int water = 0;
        
        for (int i = 0; i < height.length; i++) {
            // ⚠️ [变化点 1] 维护单调递减栈
            // 当当前高度 > 栈顶高度,说明可能形成凹槽
            while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                int bottom = stack.pop(); // 凹槽底部
                
                // ⚠️ [变化点 2] 如果栈空,说明没有左壁,无法形成凹槽,直接跳过
                if (stack.isEmpty()) break;
                
                int left = stack.peek(); // 左壁索引
                int right = i;           // 右壁索引
                
                // ⚠️ [变化点 3] 计算凹槽的水量
                int h = Math.min(height[left], height[right]) - height[bottom];
                int w = right - left - 1;
                
                water += h * w;
            }
            stack.push(i);
        }
        
        return water;
    }
}

🔄 变体 3:LeetCode 20. 有效的括号 (Valid Parentheses)

题目内容:给定一个只包括 '(', ')', '{', '}', '[', ']' 的字符串 s ,判断字符串是否有效。
相比母题的变化

  1. 栈的用途非单调栈。纯粹利用 LIFO 特性进行匹配抵消
  2. 逻辑
    • 遇左括号:入栈。
    • 遇右括号:检查栈顶是否为对应的左括号。
      • 是:弹出栈顶(匹配成功)。
      • 否:或栈空:直接返回 false
  3. 结果:遍历结束后,栈必须为空。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;

class Solution {
    public boolean isValid(String s) {
        // ⚠️ [变化点 1] 栈用于存储左括号,进行匹配
        Deque<Character> stack = new ArrayDeque<>();
        
        // 建立右括号到左括号的映射,方便检查
        Map<Character, Character> map = new HashMap<>();
        map.put(')', '(');
        map.put(']', '[');
        map.put('}', '{');
        
        for (char c : s.toCharArray()) {
            if (map.containsKey(c)) {
                // ⚠️ [变化点 2] 遇到右括号
                // 如果栈空 或 栈顶不匹配,则无效
                if (stack.isEmpty() || stack.peek() != map.get(c)) {
                    return false;
                }
                stack.pop(); // 匹配成功,弹出
            } else {
                // 遇到左括号,入栈
                stack.push(c);
            }
        }
        
        // ⚠️ [变化点 3] 最终栈必须为空才有效
        return stack.isEmpty();
    }
}

🔄 变体 4:LeetCode 155. 最小栈 (Min Stack)

题目内容:设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。
相比母题的变化

  1. 数据结构:使用双栈
    • dataStack:存储所有数据。
    • minStack:同步存储当前状态下的最小值
  2. Push 逻辑
    • dataStack 正常压入。
    • minStack 压入 min(x, minStack.peek())(如果 minStack 为空则直接压入 x)。
  3. Pop 逻辑:两个栈同时弹出。
import java.util.ArrayDeque;
import java.util.Deque;

class MinStack {
    private Deque<Integer> dataStack;
    private Deque<Integer> minStack;

    public MinStack() {
        dataStack = new ArrayDeque<>();
        minStack = new ArrayDeque<>();
    }
    
    public void push(int val) {
        dataStack.push(val);
        // ⚠️ [变化点 1] 维护最小值栈
        // 如果 minStack 为空,或者当前值 <= 栈顶最小值,则压入当前值
        // 注意:这里必须用 <=,保证如果有多个相同的最小值,pop 时能正确对应
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        } else {
            // 否则,重复压入当前的最小值,保持两个栈大小一致
            minStack.push(minStack.peek());
        }
    }
    
    public void pop() {
        // ⚠️ [变化点 2] 双栈同时弹出
        if (!dataStack.isEmpty()) {
            dataStack.pop();
            minStack.pop();
        }
    }
    
    public int top() {
        return dataStack.peek();
    }
    
    public int getMin() {
        return minStack.peek();
    }
}

🔄 变体 4:LeetCode 239. 滑动窗口最大值 (Sliding Window Maximum)

题目内容:给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。
核心逻辑(单调队列)

  1. 数据结构:使用 Deque (双端队列),而非普通栈。
  2. 单调性:维护一个单调递减队列。队头始终是当前窗口的最大值。
  3. 操作
    • 入队:新元素进入窗口前,从队尾开始,把所有比它小的元素都弹出(因为它们不可能再成为最大值了),然后新元素入队尾。
    • 出队:检查队头元素是否已滑出窗口(索引 <= i - k),如果是,则从队头弹出。
    • 取值:队头即为当前窗口最大值。
import java.util.ArrayDeque;
import java.util.Deque;

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0) return new int[0];
        
        int n = nums.length;
        int[] res = new int[n - k + 1];
        int idx = 0;
        
        // ⚠️ [核心数据结构] 双端队列,存储【索引】
        // 队列保持单调递减:队头是最大的元素索引
        Deque<Integer> deque = new ArrayDeque<>();
        
        for (int i = 0; i < n; i++) {
            // 1. 维护单调性:移除队尾所有小于当前元素的索引
            while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
                deque.pollLast();
            }
            deque.offerLast(i);
            
            // 2. 移除过期元素:如果队头索引已经滑出窗口 (i - k),则移除
            if (deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }
            
            // 3. 记录结果:当窗口形成 (i >= k - 1) 时,队头即为最大值
            if (i >= k - 1) {
                res[idx++] = nums[deque.peekFirst()];
            }
        }
        
        return res;
    }
}
posted @ 2026-03-08 21:31  scales123  阅读(51)  评论(0)    收藏  举报