目录
- 概述
- 基本思想
- 基本操作
- push() 操作
- getMax()操作
- pop()操作
- 总结
- 复杂度分析
- 案例解答
- 附录
特殊的数据结构就是用来处理特殊的问题,介绍单调队列之前,先来查看这样一种问题
给你一个整数数组 nums,有一个大小为 k 的窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位,编程输出每 $k$ 个连续的数种的最大值和最小值
最朴实的想法就是:对于每一段长度为$i\sim i+k-1$ 的子数组,逐个比较来找出最大值和最小值,最终实践复杂度约为 $O(n \times k)$,这其中进行了大量的比较工作,那么是否存在一种比较边界的方法尼?答案是:存在,就是单调队列
概述
❓什么是单调队列?
单调队列仍然是一个队列,不过是对队列增强了一些限制(队列中的元素必须是单调增加或者单调降低的),与单调栈的关系类似。单调队列只是使用了一点巧妙的方法,使得队列中的元素单调递增(或递减)
✨单调队列主要用于解决滑动窗口类问题的数据结构,即:在长度为 $n$ 的序列中,求每个长度为 $m$ 的区间的区间最值,它的时间复杂度是 $O(n)$
基本思想
单调队列的基本思想是:维护一个双向队列(deque),遍历序列,仅当一个元素可能成为某个区间最值时才保留它
基本操作
一个单调队列提供的 API 原型如下,为了更好的演示,默认单调队列是单调递减的(队头元素是当前窗口的最大值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class MonotonicQueue { LinkedList<Integer> monotonicQueue;
public MonotonicQueue() {}
public void push(int value) {}
public int getMax() {}
public void pop(int value) {} }
|
💡对于这种取值,查值的操作,通常存储每个值的索引比存储值本身带来的效果更好,由于需要演示,所以这里采用的还是存储值的方式,而不是存索引的方式
push()
操作
向单调队列的尾部添加一个元素,如果添加的元素比队尾元素大,弹出队尾元素
1 2 3 4 5 6 7
| public void push(int value) { while (!monotonicQueue.isEmpty() && monotonicQueue.peekLast() < value) { monotonicQueue.removeLast(); } monotonicQueue.offer(value); }
|
getMax()
操作
最终单调队列中的元素大小会保持一个单调递减的顺序,因此直接获取队列头结点就可以了(不考虑队列空的情况)
1 2 3 4 5 6
|
public int getMax() { return monotonicQueue.peekFirst(); }
|
pop()
操作
删除操作是一种伪操作,由于某一个元素在入队的时候,可能造成其他元素的出队,所以这里的出队操作就是检查要出队的元素否为最大值(只有最大值的才会真正的出队,如果当前元素并不是最大元素,一定已经在最大元素入队的时候删除)
1 2 3 4 5 6
| public void pop(int value) { if (!monotonicQueue.isEmpty() && monotonicQueue.peekFirst() == value) { monotonicQueue.removeFirst(); } }
|
总结
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class MonotonicQueue { LinkedList<Integer> monotonicQueue;
public MonotonicQueue() { monotonicQueue = new LinkedList<>(); }
public void push(int value) { while (!monotonicQueue.isEmpty() && monotonicQueue.peekLast() < value) { monotonicQueue.removeLast(); } monotonicQueue.offer(value); }
public int getMax() { return monotonicQueue.peekFirst(); }
public void pop(int value) { if (!monotonicQueue.isEmpty() && monotonicQueue.peekFirst() == value) { monotonicQueue.removeFirst(); } } }
|
复杂度分析
算法整体的复杂度依然是 $O(N)$ 线性时间,空间复杂度是 $O(K)$ 额外花费在窗口大小
案例解答
这里主要对上面的例题中求最大值进行coding
,最小值一个思路,不过就是在插入元素时处理方式不同而已
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public int[] maxSlidingWindow(int[] nums, int k) { int[] maxRecord = new int[nums.length - k + 1]; LinkedList<Integer> monotonicQueue = new LinkedList<>(); for (int i = 0; i < k; i++) { while (!monotonicQueue.isEmpty() && nums[monotonicQueue.peekLast()] < nums[i]) { monotonicQueue.removeLast(); } monotonicQueue.offer(i); } maxRecord[0] = nums[monotonicQueue.peekFirst()]; for (int i = k; i < nums.length; i++) { while (!monotonicQueue.isEmpty() && nums[monotonicQueue.peekLast()] < nums[i]) { monotonicQueue.removeLast(); } monotonicQueue.offer(i); while (monotonicQueue.peekFirst() <= (i - k)) { monotonicQueue.removeFirst(); } maxRecord[i - k + 1] = nums[monotonicQueue.peekFirst()]; } return maxRecord; }
|
其实以上代码就是LeetCode.239. 滑动窗口最大值
附录
算法学习笔记(66): 单调队列
特殊数据结构:单调队列
单调队列