文章目录
- 单调队列结构解决滑动窗口问题
- 什么是单调队列?
- [239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/)
- 单调队列框架
- 滑动窗口解题框架
- 完整的解题代码如下:
- 我的实现:
单调队列结构解决滑动窗口问题
什么是单调队列?
单调队列其实就是一个队列,只是使用了一点巧妙的方法使得队列中的元素全都是单调递增(或单调递减)的
单调队列主要解决以下问题:
给你一个数组
window
,已知其最值为A
,如果给window
中添加一个数B
,那么比较一下A
和B
就可以立即算出新的最值;但如果要从window
数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是A
,就需要遍历window
中的所有元素重新寻找新的最值。针对这个问题我们可以用优先级队列解决吗?
我们都知道优先级队列是一个特殊的队列,专门用来动态寻找最值得
但是如果只是淡出维护最值得话,优先级队列很专业,队头元素就是最值。
不过,优先级队列无法满足标准队列结构先进先出的时间顺序,因为优先级队列底层利用二叉堆进行动态排序,元素的出对顺序是元素的大小顺序和入队的先后顺序完全没有关系
因此我们用单调队列这种新的队列结构,既能维护队列元素先进先出的时间顺序又能够正确维护队列中所有元素的最值
总结:单调队列这个数据结构主要用来辅助解决滑动窗口相关的问题的。之前有关滑动窗口的问题我们使用的是双指针技巧但是有些稍微复杂的滑动窗口问题是不能只靠两个指针来解决的,需要更先进的数据结构
比如说每当窗口扩大和窗口缩小时,单凭移除和移入窗口的元素即可决定是否更新答案,但是当要从窗口中减少一个元素,我们无法单凭溢出的那个元素更新窗口的最值,除非重新遍历所有元素,但是这样就会增加时间复杂度
239. 滑动窗口最大值
对于这个问题我们可以利用单调队列结构用O(1)时间算出每个滑动窗口中的最大值使得整个算法在线性时间完成
单调队列框架
首先普通的队列的框架一般是这样的
class Queue {
// enqueue 操作,在队尾加入元素 n
void push(int n);
// dequeue 操作,删除队头元素
void pop();
}
而单调队列的框架也是差不多的
class Queue {
// enqueue 操作,在队尾加入元素 n
void push(int n);
// dequeue 操作,删除队头元素
void pop();
}
虽然是差不多但是实际的实现跟普通队列还是不一样的
观察滑动窗口的过程很容易发现,实现单调队列必须使用一种数据结构支持在头部和尾部进行插入和删除,而双链表满足这个条件
单调队列的核心思想和单调栈类似,push方法依然在队尾添加元素,但是要把前面比自己小的元素都删除掉
class MonotonicQueue {
// 双链表,支持头部和尾部增删元素
// 维护其中的元素自尾部到头部单调递增
private LinkedList<Integer> maxq = new LinkedList<>();
// 在尾部添加一个元素 n,维护 maxq 的单调性质
public void push(int n) {
// 将前面小于自己的元素都删除
while (!maxq.isEmpty() && maxq.getLast() < n) {
maxq.pollLast();
}
maxq.addLast(n);
}
我们可以这样理解:
加入数字的大小相当于人的体重,把前面体重不足的都压扁了,知道遇到更大的两集才停住
如果每个元素被加入时都是这样的操作,最终单调队列中的元素大小就会保持一个单调递减的顺序,所以max方法可以如下:
class MonotonicQueue {
// 为了节约篇幅,省略上文给出的代码部分...
public int max() {
// 队头的元素肯定是最大的
return maxq.getFirst();
}
}
pop方法在队头删除元素n也很好写,但是我们要判断 data.getFirst() == n
,这是因为我们想删除的队头元素n可能已经被压扁了,即可能不存在了,所以这时候就不用删除
class MonotonicQueue {
// 为了节约篇幅,省略上文给出的代码部分...
public void pop(int n) {
if (n == maxq.getFirst()) {
maxq.pollFirst();
}
}
}
滑动窗口解题框架
int[] maxSlidingWindow(int[] nums, int k) {
MonotonicQueue window = new MonotonicQueue();
List<Integer> res = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
if (i < k - 1) {
//先把窗口的前 k - 1 填满
window.push(nums[i]);
} else {
// 窗口开始向前滑动
// 移入新元素
window.push(nums[i]);
// 将当前窗口中的最大元素记入结果
res.add(window.max());
// 移出最后的元素
window.pop(nums[i - k + 1]);
}
}
// 将 List 类型转化成 int[] 数组作为返回值
int[] arr = new int[res.size()];
for (int i = 0; i < res.size(); i++) {
arr[i] = res.get(i);
}
return arr;
}
完整的解题代码如下:
/* 单调队列的实现 */
class MonotonicQueue {
LinkedList<Integer> maxq = new LinkedList<>();
public void push(int n) {
// 将小于 n 的元素全部删除
while (!maxq.isEmpty() && maxq.getLast() < n) {
maxq.pollLast();
}
// 然后将 n 加入尾部
maxq.addLast(n);
}
public int max() {
return maxq.getFirst();
}
public void pop(int n) {
if (n == maxq.getFirst()) {
maxq.pollFirst();
}
}
}
/* 解题函数的实现 */
int[] maxSlidingWindow(int[] nums, int k) {
MonotonicQueue window = new MonotonicQueue();
List<Integer> res = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
if (i < k - 1) {
//先填满窗口的前 k - 1
window.push(nums[i]);
} else {
// 窗口向前滑动,加入新数字
window.push(nums[i]);
// 记录当前窗口的最大值
res.add(window.max());
// 移出旧数字
window.pop(nums[i - k + 1]);
}
}
// 需要转成 int[] 数组再返回
int[] arr = new int[res.size()];
for (int i = 0; i < res.size(); i++) {
arr[i] = res.get(i);
}
return arr;
}
细节问题的解决:
在实现单调队列的时候,我们使用了Java的LinkedList,因为链表结构支持在头部和尾部快速增删元素,而在解法代码中的res则使用的ArrayList结构,因为后序会按照索引取元素,所以数组结构更合适
时间复杂度问题的解决:
在push操作中有while循环,时间复杂度还是O(1)吗?
其实单独看push操作的复杂度确实不是O(1),但是算法啊整体的复杂度依然是O(N)线性时间。因为nums中的每个元素最多被push和pop一次,没有任何多余操作,所以整体的时间复杂度还是O(N)。
空间复杂度是多少
空间复杂度就是窗口的大小O(K)
其他问题和思考(也就是单调队列的通用实现)
- 本次单调队列类只实现了max方法,你是否能够再额外添加一个min方法,在O(1)的时间返回队列中所有元素的最小值?
- 本次单调队列类的pop方法还需要接收一个参数,这显然有悖于标准队列的做法,如何修复这个缺陷?
- 如何实现单调队列类中的size方法,返回单调队列中元素的个数(注意,由于每次push方法都可能从底层的q列表中删除元素,所以q中的元素个数并不是单调队列的元素个数)
我的实现:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
//实现单调链表
MonotonicQueue mq=new MonotonicQueue();
List<Integer> res=new ArrayList<>();
for(int i=0;i<nums.length;i++){
if(i<k-1){
//先填满窗口
mq.push(nums[i]);
}else{
//插入新的元素
mq.push(nums[i]);
//在结构列表中加入最大值
res.add(mq.max());
//删除旧的元素
mq.pop(nums[i-k+1]);
}
}
///将结果转成int数组
int[] result=new int[res.size()];
for(int j=0;j<res.size();j++){
result[j]=res.get(j);
}
return result;
}
}
class MonotonicQueue{
//底层是双链表
Deque<Integer> maxq=new LinkedList<>();
//在队尾插入元素,如果队尾前面有小于被插入元素的数要被删除,直到遇到比它大的就不用再删除了
public void push(int n){
while(!maxq.isEmpty()&&maxq.getLast()<n){
maxq.pollLast();
}
//到了可以插入的位置,插入需要被插入的元素
maxq.addLast(n);
}
//最大值就是队头元素
public int max(){
return maxq.getFirst();
}
//在对头删除元素,如果队头元素不是n的话就不需要删除了,因为可能已经被删除掉了
public void pop(int n){
if(n==maxq.getFirst()){
maxq.pollFirst();
}
}
}