剑指 Offer 59 - II. 队列的最大值
1、题目
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
2、题解
这题需要用到单调递减队列
如何更快的找到队列中的最大值?
队列中没必要维护窗口中的所有元素,我们可以在队列中只保留那些可能成为窗口中的最大元素,去掉那些不可能成为窗口中的最大元素。
如果新进来的数字大于滑动窗口的末尾元素,那么末尾元素就不可能再成为窗口中最大的元素了,因为这个大的数字是后进来的,一定会比之前先进入窗口的小的数字要晚离开窗口,因此我们就可以将滑动窗口中比其小的数字弹出队列,于是队列中的元素就会维持从队头到队尾单调递减,这就是单调递减队列。
为了实现此递减列表,需要使用 双向队列 ,假设队列已经有若干元素:
- 当执行入队
push_back()
时: 若入队一个比队列某些元素更大的数字 x ,则为了保持此列表递减,需要将双向队列 尾部所有小于 x 的元素 弹出。 - 当执行出队
pop_front()
时: 若出队的元素是最大元素,则 双向队列 需要同时 将首元素出队 ,以保持队列和双向队列的元素一致性。
3、代码
class MaxQueue {
Deque<Integer> queue;
Deque<Integer> help;
public MaxQueue() {
queue = new ArrayDeque<>();
help = new ArrayDeque<>();
}
public int max_value() {
return help.isEmpty() ? -1 : help.peek();
}
public void push_back(int value) {
queue.offer(value);
while(!help.isEmpty() && help.peekLast() < value){
help.pollLast();
}
help.offerLast(value);
}
public int pop_front() {
if(queue.isEmpty()) return -1;
int val = queue.poll();
if(help.peek() == val){
help.poll();
}
return val;
}
}
剑指 Offer 59 - I. 滑动窗口的最大值
1、题目
给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
2、题解
解法一、大根堆
维护一个大小为窗口大小的大顶堆,顶堆元素则为当前窗口的最大值。
假设窗口的大小为 M,数组的长度为 N。在窗口向右移动时,需要先在堆中删除离开窗口的元素,并将新到达的元素添加到堆中.
解法二:单调递减队列
首先,我们可以想到最朴素的做法是模拟滑动窗口的过程,每向右滑动一次都遍历一遍滑动窗口,找到最大的元素输出,这样的时间复杂度是O(nk)O(nk)。考虑优化,其实滑动窗口类似于数据结构双端队列,窗口向右滑动过程相当于向队尾添加新的元素,同时再把队首元素删除。
如何更快的找到队列中的最大值?
队列中没必要维护窗口中的所有元素,我们可以在队列中只保留那些可能成为窗口中的最大元素,去掉那些不可能成为窗口中的最大元素。
如果新进来的数字大于滑动窗口的末尾元素,那么末尾元素就不可能再成为窗口中最大的元素了,因为这个大的数字是后进来的,一定会比之前先进入窗口的小的数字要晚离开窗口,因此我们就可以将滑动窗口中比其小的数字弹出队列,于是队列中的元素就会维持从队头到队尾单调递减,这就是单调递减队列。
对于队列内的元素来说:
- 在队列内自己左边的数就是数组中左边第一个比自己大的元素。
- 当被弹出时,遇到的就是数组中右边第一个比自己大的元素 ,只要元素还在队列中,就意味着暂时还没有数组中找到自己右侧比自己大的元素。
- 队头到队尾单调递减,队首元素为队列最大值。
算法流程:
- 初始化: 双端队列 queue,结果列表 res ,数组长度 len ;
- 滑动窗口: 左边界范围
i ∈ [1 − k,len − k]
,右边界范围j ∈ [0, len − 1] ;
- 队首元素 deque[0] == 被删除元素 nums[i - 1]:则队首元素出队;
- 删除 queue 内所有 < nums[j] 的元素,以保持 queue 递减;
- 将 nums[j] 添加至 queue 尾部;
- 若已形成窗口:将窗口最大值(即队首元素 deque[0])添加至列表 res ;
- 返回值: 返回结果列表 res ;
3、代码
解法一:大根堆
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];
if(k > nums.length || k < 1 || nums.length == 0){
return new int[0];
}
PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);
for(int i = 0;i < k;i++){
heap.add(nums[i]);
}
res[0] = heap.peek();
for(int i = 0, j = i + k;j < nums.length;i++,j++) {//维护一个大小为 k 的堆
heap.remove(nums[i]);//删除堆顶元素
heap.add(nums[j]);//将新到达的元素添加到堆中
res[i + 1] = heap.peek();
}
return res;
}
}
解法二:单调递减队列
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length;
Deque<Integer> queue = new ArrayDeque<>(); //双端队列
if(k > nums.length || k < 1 || nums.length == 0){
return new int[0];
}
int[] res = new int[len - k + 1];
int index = 0;//数组下标
//未形成窗口
for(int i = 0;i < k;i++){
//一直循环删除到队列中的值都大于当前值,或者删到队列为空
while(!queue.isEmpty() && queue.peekLast() < nums[i]){
queue.removeLast();
}
//执行完上面的循环后,队列中要么为空,要么值都比当前值大,然后就把当前值添加到队列中
queue.addLast(nums[i]);
}
res[index++] = queue.peekFirst();
//形成窗口后
for(int i = k;i < len;i++){
//i-k是已经在区间 k 外了,如果首位等于nums[i-k],那么说明此时首位值已经不再区间内了,需要删除
if(queue.peekFirst() == nums[i - k]){
queue.removeFirst();
}
while(!queue.isEmpty() && queue.peekLast() < nums[i]){
queue.removeLast();
}
queue.addLast(nums[i]);
//把队列的首位值添加到arr数组中
res[index++] = queue.peekFirst();
}
return res;
}
}