题目介绍
接下来,我们就借助单调队列结构,用O(1)时间算出每个滑动窗口中的最大值,使得整个算法在线性时间完成。
分析
在介绍「单调队列」这种数据结构的 API 之前,先来看看一个普通的队列的标准 API:
class Queue {
// enqueue 操作,在队尾加入元素 n
void push(int n);
// dequeue 操作,删除队头元素
void pop();
}
我们要实现的「单调队列」的 API 也差不多:
class MonotonicQueue {
// 在队尾添加元素 n
void push(int n);
// 返回当前队列中的最大值
int max();
// 队头元素如果是 n,删除它
void pop(int n);
}
当然,这几个 API 的实现方法肯定跟一般的 Queue 不一样,不过我们暂且不管,而且认为这几个操作的时间复杂度都是 O(1),先把这道「滑动窗口」问题的解答框架搭出来:
public 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;
}
这个思路很简单,能理解吧?下面我们开始重头戏,单调队列的实现。
观察滑动窗口的过程就能发现,实现「单调队列」必须使用一种数据结构支持在头部和尾部进行插入和删除,很明显双链表是满足这个条件的。
「单调队列」的核心思路和「单调栈」类似,push方法依然在队尾添加元素,但是要把前面比自己小的元素都删掉:
/**
* 单调队列的实现
*/
class MonotonicQueue {
//双向链表,支持头部和尾部增删元素
//维护其中的元素自尾部到头单调递增
LinkedList<Integer> queue = new LinkedList<>();
/**
* 往单调队列中添加元素值n
*/
public void push(int n) {
//将小于n的元素全部删除
while(!queue.isEmpty() && queue.getLast() < n) {
queue.pollLast();
}
//然后将n加入到队尾
queue.addLast(n);
}
}
你可以想象,加入数字的大小代表人的体重,把前面体重不足的都压扁了,直到遇到更大的量级才停住。
如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个单调递减的顺序,因此我们的max方法可以可以这样写:
/**
* 获取单调队列中元素最大值
*/
public int max() {
return queue.getFirst();
}
pop方法在队头删除元素n,也很好写:
/**
* 从单调队列中移除元素值n
*/
public void pop(int n) {
if(n == queue.getFirst()) {
queue.pollFirst();
}
}
之所以要判断data.getFirst() == n,是因为我们想删除的队头元素n可能已经被「压扁」了,可能已经不存在了,所以这时候就不用删除了:
完整代码如下:
class Solution {
public 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;
}
}
/**
* 单调队列的实现
*/
class MonotonicQueue {
LinkedList<Integer> queue = new LinkedList<>();
/**
* 往单调队列中添加元素值n
*/
public void push(int n) {
//将小于n的元素全部删除
while(!queue.isEmpty() && queue.getLast() < n) {
queue.pollLast();
}
//然后将n加入到队尾
queue.addLast(n);
}
/**
* 获取单调队列中元素最大值
*/
public int max() {
return queue.getFirst();
}
/**
* 从单调队列中移除元素值n
*/
public void pop(int n) {
if(n == queue.getFirst()) {
queue.pollFirst();
}
}
}