📊 单调栈与单调队列 — 柱状图与滑动最大值

柱状图最大矩形、每日温度、滑动窗口最大值——单调数据结构的威力

📖 单调栈与单调队列的本质

单调栈和单调队列是维护序列中元素单调性的数据结构。它们的核心思想是:当一个新元素到来时,移除所有"不再有用"的旧元素,只保留对未来有用的信息。

单调栈 vs 单调队列: 单调栈 — 处理"下一个更大/更小元素" ├── 单调递增栈: 找下一个更小元素 ├── 单调递减栈: 找下一个更大元素 └── 应用: 柱状图最大矩形, 每日温度, 接雨水 单调队列 — 处理"滑动窗口最大/最小值" ├── 单调递减队列: 窗口最大值 ├── 单调递增队列: 窗口最小值 └── 应用: 滑动窗口最大值, 滑动窗口中位数 核心思想: 新元素到来 → 移除"被淘汰"的旧元素 被淘汰 = 永远不可能成为答案的元素 时间复杂度: O(n) — 每个元素最多入栈/出栈一次

🎯 题目一:柱状图中的最大矩形 (LC 84)

题目描述:给定一个非负整数数组 heights,表示柱状图中各个柱子的高度,每个柱子宽度为1,求柱状图中能勾勒出的最大矩形面积。

示例: heights = [2,1,5,6,2,3] 柱子: ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ 对每个柱子, 找到左右第一个更矮的柱子: i=0: h=2, left=-1, right=1 → 面积=2×(1-(-1)-1)=2×1=2 i=1: h=1, left=-1, right=6 → 面积=1×(6-(-1)-1)=1×6=6 i=2: h=5, left=1, right=4 → 面积=5×(4-1-1)=5×2=10 i=3: h=6, left=2, right=4 → 面积=6×(4-2-1)=6×1=6 i=4: h=2, left=1, right=6 → 面积=2×(6-1-1)=2×4=8 i=5: h=3, left=4, right=6 → 面积=3×(6-4-1)=3×1=3 最大面积: 10 (柱子2和3构成的区域) 单调栈过程 (加入哨兵0): heights = [2,1,5,6,2,3,0] i=0 h=2: stack=[0] i=1 h=1: 2>1, 弹出0, left=-1, area=2×1=2 stack=[1] i=2 h=5: stack=[1,2] i=3 h=6: stack=[1,2,3] i=4 h=2: 6>2, 弹出3, left=2, area=6×1=6 5>2, 弹出2, left=1, area=5×2=10 ← 最优! stack=[1,4] i=5 h=3: stack=[1,4,5] i=6 h=0: 弹出剩余...

思路分析

单调递增栈:栈中保存索引,对应的柱子高度递增。当遇到比栈顶矮的柱子时,栈顶柱子的"右边界"就确定了——就是当前柱子。弹出栈顶后,新的栈顶就是"左边界"。面积 = height[弹出] × (right - left - 1)。
哨兵技巧:在数组末尾加一个高度0的哨兵,确保所有柱子最终都会被弹出处理。也可以在开头加哨兵避免空栈判断。

代码实现

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        stack = []  # 单调递增栈(存索引)
        max_area = 0
        heights.append(0)  # 哨兵,确保所有柱子都会被弹出
        
        for i, h in enumerate(heights):
            while stack and heights[stack[-1]] > h:
                height = heights[stack.pop()]
                # 左边界: 栈顶(弹出后) 或 -1(栈空)
                left = stack[-1] if stack else -1
                width = i - left - 1
                max_area = max(max_area, height * width)
            stack.append(i)
        
        heights.pop()  # 恢复
        return max_area

复杂度分析

时间复杂度:O(n),空间复杂度:O(n)

LC 84 的核心理解:对于柱子 i,以它为高的最大矩形,宽度由"左边第一个更矮"和"右边第一个更矮"决定。单调递增栈恰好维护了这个信息——弹出时左右边界同时确定。

🎯 题目二:每日温度 (LC 739)

题目描述:给定一个整数数组 temperatures,表示每天的温度,返回数组 answer,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。

示例: temperatures = [73,74,75,71,69,72,76,73] 单调递减栈过程: Day0 73: stack=[0] Day1 74: 73<74, 弹出0, ans[0]=1-0=1 stack=[1] Day2 75: 74<75, 弹出1, ans[1]=2-1=1 stack=[2] Day3 71: stack=[2,3] Day4 69: stack=[2,3,4] Day5 72: 69<72, 弹出4, ans[4]=5-4=1 71<72, 弹出3, ans[3]=5-3=2 stack=[2,5] Day6 76: 72<76, 弹出5, ans[5]=6-5=1 75<76, 弹出2, ans[2]=6-2=4 stack=[6] Day7 73: stack=[6,7] 结果: [1,1,4,2,1,1,0,0]

代码实现

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        n = len(temperatures)
        answer = [0] * n
        stack = []  # 单调递减栈(存索引)
        
        for i, temp in enumerate(temperatures):
            while stack and temperatures[stack[-1]] < temp:
                prev = stack.pop()
                answer[prev] = i - prev
            stack.append(i)
        
        return answer

复杂度分析

时间复杂度:O(n),空间复杂度:O(n)

LC 739 是"下一个更大元素"的经典模板。扩展:(1) 循环数组的下一个更大元素 (LC 503)——将数组拼接两遍再跑单调栈;(2) 下一个更大元素 II (LC 496)——用哈希表记录映射。

🎯 题目三:滑动窗口最大值 (LC 239) — 单调队列

题目描述:给定一个数组和滑动窗口大小 k,返回每个窗口中的最大值。

单调递减队列工作原理: nums = [1,3,-1,-3,5,3,6,7], k = 3 ┌──────────────────────────────────────────┐ │ Step 队列(索引) 对应值 窗口范围 最大值│ ├──────────────────────────────────────────┤ │ i=0 [0] [1] │ │ i=1 [1] [3] (1>3被淘汰) │ │ i=2 [1,2] [3,-1] [0..2] 3 │ │ i=3 [1,2,3] [3,-1,-3] [1..3] 3 │ │ i=4 [4] [5] [2..4] 5 │ │ (1过期, -1,-3<5被淘汰) │ │ i=5 [4,5] [5,3] [3..5] 5 │ │ i=6 [6] [6] [4..6] 6 │ │ i=7 [7] [7] [5..7] 7 │ └──────────────────────────────────────────┘ 两步操作: 1. 入队: 从队尾移除所有 ≤ 当前元素的 2. 出队: 如果队首索引超出窗口范围则移除

代码实现

from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        dq = deque()  # 单调递减队列(存索引)
        result = []
        
        for i, num in enumerate(nums):
            # 1. 维护单调性:移除队尾比当前小的
            while dq and nums[dq[-1]] < num:
                dq.pop()
            dq.append(i)
            
            # 2. 移除过期元素
            if dq[0] <= i - k:
                dq.popleft()
            
            # 3. 记录结果
            if i >= k - 1:
                result.append(nums[dq[0]])
        
        return result

复杂度分析

时间复杂度:O(n),空间复杂度:O(k)

🔬 单调栈/队列的完整模板

单调栈模板 — 找下一个更大元素

# 下一个更大元素(单调递减栈)
def next_greater(nums):
    n = len(nums)
    result = [-1] * n
    stack = []  # 递减栈,存索引
    
    for i, num in enumerate(nums):
        while stack and nums[stack[-1]] < num:
            result[stack.pop()] = num
        stack.append(i)
    
    return result

# 变体: 循环数组的下一个更大元素 (LC 503)
def next_greater_circular(nums):
    n = len(nums)
    result = [-1] * n
    stack = []
    
    for i in range(2 * n):
        while stack and nums[stack[-1]] < nums[i % n]:
            result[stack.pop()] = nums[i % n]
        if i < n:
            stack.append(i)
    
    return result

单调栈模板 — 柱状图最大矩形

# 柱状图最大矩形(单调递增栈)
def largest_rectangle(heights):
    stack = []
    max_area = 0
    heights = heights + [0]  # 哨兵
    
    for i, h in enumerate(heights):
        while stack and heights[stack[-1]] > h:
            height = heights[stack.pop()]
            left = stack[-1] if stack else -1
            max_area = max(max_area, height * (i - left - 1))
        stack.append(i)
    
    return max_area

单调队列模板 — 滑动窗口最大值

# 滑动窗口最大值(单调递减队列)
from collections import deque

def sliding_max(nums, k):
    dq = deque()
    result = []
    
    for i, num in enumerate(nums):
        while dq and nums[dq[-1]] < num:
            dq.pop()
        dq.append(i)
        if dq[0] <= i - k:
            dq.popleft()
        if i >= k - 1:
            result.append(nums[dq[0]])
    
    return result
单调栈/队列的核心就一句话:新元素到来时,移除所有"不再有前途"的旧元素。递减栈中比新元素小的都不可能成为"下一个更大"的答案;递增栈中比新元素大的都不可能成为矩形的左边界。理解了这个,一切单调栈/队列问题都能推导出来。

🔬 LC 84 的变体:最大矩形 (LC 85)

LC 85 是 LC 84 的2D版本——给一个0/1矩阵,求只含1的最大矩形面积。

降维转化:逐行处理,把当前行及以上的连续1看作"柱子高度"。例如矩阵第3行为 [1,0,1,1],则第3行的柱子高度为上面连续1的个数。然后对每行调用 LC 84 的单调栈解法即可。
class Solution:
    def maximalRectangle(self, matrix: List[List[str]]) -> int:
        if not matrix: return 0
        n = len(matrix[0])
        heights = [0] * n
        max_area = 0
        
        for row in matrix:
            for j in range(n):
                heights[j] = heights[j] + 1 if row[j] == '1' else 0
            
            # 复用LC 84的单调栈
            stack = []
            for i, h in enumerate(heights + [0]):
                while stack and heights[stack[-1]] > h:
                    height = heights[stack.pop()]
                    left = stack[-1] if stack else -1
                    max_area = max(max_area, height * (i - left - 1))
                stack.append(i)
        
        return max_area

📝 练习题单

  1. LC 84 柱状图中最大矩形(单调递增栈)✅
  2. LC 739 每日温度(单调递减栈)✅
  3. LC 239 滑动窗口最大值(单调递减队列)✅
  4. LC 85 最大矩形(LC 84的2D版本)
  5. LC 42 接雨水(单调栈解法)
  6. LC 496 下一个更大元素 I
  7. LC 503 下一个更大元素 II(循环数组)
  8. LC 901 股票价格跨度(单调栈应用)
  9. LC 1438 绝对差不超过限制的最长连续子数组(单调队列×2)
  10. LC 862 和至少为K的最短子数组(单调队列+前缀和)

🔑 关键知识点总结

成就解锁:单调结构大师 — 掌握单调栈(递增/递减)和单调队列的核心模式,能独立解决柱状图和滑动窗口问题!🎉
LeetCode AC验证:LC 84 Largest Rectangle ✅ | LC 739 Daily Temperatures ✅ | LC 239 Sliding Window Max ✅
思考题:为什么 LC 84 用单调递增栈,而 LC 739 用单调递减栈?如果把 LC 84 改成用单调递减栈,会怎样?提示:思考"弹出时确定了什么信息"——递增栈弹出时确定右边界,递减栈弹出时确定"下一个更大"。