高阶算法技巧分享三:单调栈与单调队列详解

高阶算法技巧:单调栈与单调队列详解

一、核心思想

单调栈单调队列 通过维护一个元素按特定顺序(递增或递减)排列的结构,将某些问题的复杂度从 O(n²) 优化到 O(n)。核心在于及时排除无效元素,减少不必要的计算


二、单调队列案例:滑动窗口最大值

问题描述 :给定数组和窗口大小 k,求每个窗口的最大值。
暴力法问题:遍历每个窗口找最大值,时间复杂度 O(nk)。

优化思路:维护一个单调递减队列,队头为当前窗口最大值。

步骤分解

  1. 队列中存储元素索引,保证对应值递减。
  2. 新元素入队前,移除队尾比它小的元素。
  3. 检查队头是否在窗口内,否则移除。
  4. 当窗口形成后(i ≥ k-1),记录队头值。

图解

ini 复制代码
数组:[1,3,-1,-3,5,3,6,7], k=3
窗口位置         队列内容(索引)        最大值
[1 3 -1]        [1,2](值3,-1)       3
1 [3 -1 -3]     [1,2,3](3,-1,-3)    3
1 3 [-1 -3 5]   [4](5)              5
...

代码示例

java 复制代码
import java.util.*;

public class SlidingWindowMax {
    public static int[] maxSlidingWindow(int[] nums, int k) {
        Deque<Integer> q = new ArrayDeque<>();
        List<Integer> result = new ArrayList<>();

        for (int i = 0; i < nums.length; i++) {
            // 维护单调递减性
            while (!q.isEmpty() && nums[q.peekLast()] <= nums[i]) {
                q.pollLast();
            }
            q.offerLast(i);

            // 移除越界的队首元素
            if (q.peekFirst() == i - k) {
                q.pollFirst();
            }

            // 窗口形成后记录结果
            if (i >= k - 1) {
                result.add(nums[q.peekFirst()]);
            }
        }

        return result.stream().mapToInt(Integer::intValue).toArray();
    }

    public static void main(String[] args) {
        int[] nums = {1,3,-1,-3,5,3,6,7};
        System.out.println(Arrays.toString(maxSlidingWindow(nums, 3)));
        // 输出: [3,3,5,5,6,7]
    }
}

三、单调栈案例1:接雨水

问题描述:计算柱子排列后能接的雨水总量。

优化思路:维护单调递减栈,遇到较高柱子时计算积水。

步骤分解

  1. 栈保存索引,对应高度递减。
  2. 当前高度 > 栈顶高度时,弹出栈顶作为坑底。
  3. 新栈顶为左边界,计算宽度和高度差。

图解

scss 复制代码
高度:[0,1,0,2,1,0,1,3,2,1,2,1]
处理索引2(高度0)时,栈为[1(1)] → 无积水。
处理索引3(高度2)时,弹出0(0),左边界1(1),积水宽度3-1-1=1,高度min(2,1)-0=1 → 积水量1×1=1。

代码示例

java 复制代码
import java.util.*;

public class TrappingRainWater {
    public static int trap(int[] height) {
        Stack<Integer> stack = new Stack<>();
        int water = 0;
        
        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 width = i - left - 1;
                int depth = Math.min(height[i], height[left]) - height[bottom];
                water += width * depth;
            }
            stack.push(i);
        }
        return water;
    }

    public static void main(String[] args) {
        int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
        System.out.println(trap(height)); // 输出: 6
    }
}

四、单调栈案例2:柱状图中的最大矩形

问题描述:找到柱状图中最大的矩形面积。

优化思路:对每个柱子,找到左右第一个比它矮的边界。

步骤分解

  1. 维护递增栈,存储(索引,高度)。
  2. 当前高度 < 栈顶高度时,确定栈顶的右边界。
  3. 左边界为栈中前一个元素,计算面积。

图解

scss 复制代码
高度:[2,1,5,6,2,3]
处理索引4(高度2)时,弹出6(3)→左边界2(5),面积6*(4-2-1)=6。
继续弹出5(2)→左边界-1,面积5*4=20。

代码示例

java 复制代码
import java.util.*;

public class LargestRectangle {
    public static int largestRectangleArea(int[] heights) {
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(-1); // 哨兵
        int maxArea = 0;
        int[] extended = Arrays.copyOf(heights, heights.length + 1);
        extended[heights.length] = 0; // 触发最终计算

        for (int i = 0; i < extended.length; i++) {
            while (stack.peek() != -1 && extended[i] < extended[stack.peek()]) {
                int h = extended[stack.pop()];
                int w = i - stack.peek() - 1;
                maxArea = Math.max(maxArea, h * w);
            }
            stack.push(i);
        }
        return maxArea;
    }

    public static void main(String[] args) {
        int[] heights = {2,1,5,6,2,3};
        System.out.println(largestRectangleArea(heights)); // 输出: 10
    }
}

五、总结与适用场景

优势

  • 时间复杂度从 O(n²) → O(n)
  • 空间复杂度 O(n)

适用场景

  • 滑动窗口最值 → 单调队列
  • 边界查找问题(雨水、矩形)→ 单调栈
  • 其他变种:下一个更大元素、股票跨度等

核心思维导图

复制代码
单调结构 → 维护有序性
   ├─ 队列:动态窗口,淘汰旧元素
   └─ 栈:历史数据,找到最近更大/小元素

掌握单调栈/队列后,可高效解决一系列看似复杂的问题,关键在于识别问题中的"无效元素"并及时剔除,聚焦核心计算。

相关推荐
照海19Gin20 分钟前
数据结构之约瑟夫环的问题
c语言·数据结构·算法
Joe_Wang522 分钟前
[leetcode]1749. 任意子数组和的绝对值的最大值(dp)
算法·leetcode
烟锁池塘柳033 分钟前
【数学建模】(启发式算法)遗传算法:自然选择的计算模型
算法·数学建模·启发式算法
Min_小明40 分钟前
CMake 简单使用总结
android·开发语言·算法
森焱森1 小时前
格雷码、汉明码,CRC校验的区别
服务器·c语言·网络·人工智能·算法
YaoSolar1 小时前
刷题记录(LeetCode 994.腐烂的橘子)
算法·leetcode·宽度优先
米芝鱼1 小时前
LearnOpenGL(九)自定义转换类
开发语言·c++·算法·游戏·图形渲染·shader·opengl
Rsecret21 小时前
个人学习编程(3-29) leetcode刷题
学习·算法·leetcode
rigidwill6663 小时前
LeetCode hot 100—LRU缓存
数据结构·c++·算法·leetcode·缓存
LuckyLay3 小时前
LeetCode算法题(Go语言实现)_15
算法·leetcode·golang