LeetCode 剑指 Offer 59 - II. 队列的最大值
题目描述

题意分析
实现先进先出的队列,支持尾部入队、头部出队,以及查询当前最大值。队列为空时,出队和查询最大值都返回
-1;每个操作要求均摊 $O(1)$ 时间。只保存一个最大值不够,因为最大值出队后还需要知道剩余元素的最大值。需要额外保留未来仍可能成为最大值的候选,并及时淘汰不可能再用到的元素。
解法:普通队列 + 单调队列
核心思路
[!blue]
普通队列
queue保存所有尚未出队的元素,保证先进先出。辅助双端队列maxDeque保存最大值候选:候选仍按入队先后排列,数值从队首到队尾单调不增,因此队首就是当前最大值。入队一个新值
value时,若候选队尾比它小,就可以删除旧候选。原因是新值更大,而且入队更晚;只要旧值尚未出队,新值就一定还在,旧值不可能再成为需要保留的最大值。不断删除严格较小的队尾后,再把新值加入末尾,就能维持单调性。这些删除只发生在候选队列,普通队列中的元素仍要按原顺序出队。相等候选必须逐份保留。当前实现存的是值,没有下标或重复次数;如果删除旧的相等值,将来旧值出队时,就无法区分并可能误删新值对应的候选。因此清理队尾使用
< value,而不是<= value。出队时,先从普通队列取出最早元素。若它等于候选队首,就同步移除队首的一份候选;若它更小,说明它此前已被后来更大的值淘汰,无需修改候选队列。查询时直接读取候选队首,两个队列为空的状态会同步保持。
每个元素最多进入候选队列一次,也最多从队尾或队首离开一次。一次入队虽然可能连续淘汰很多候选,但这些候选之后不会再被处理,所以一串操作的总维护次数是线性的。
解题步骤
- 初始化普通队列和候选双端队列,二者都为空。
push_back(value):从候选队尾连续删除严格小于value的元素,再把value加入两个队列的尾部。pop_front():普通队列为空就返回-1;否则取出普通队首,若它等于候选队首,再从候选队首删除一份,最后返回该值。max_value():候选队列为空就返回-1,否则返回队首。
代码实现
class MaxQueue {
private final Deque<Integer> queue = new ArrayDeque<>();
private final Deque<Integer> maxDeque = new ArrayDeque<>();
public MaxQueue() {}
public int max_value() {
return maxDeque.isEmpty() ? -1 : maxDeque.peekFirst();
}
public void push_back(int value) {
// 候选存值,严格较小才淘汰,相等最大值必须逐份保留
while (!maxDeque.isEmpty() && maxDeque.peekLast() < value) {
maxDeque.pollLast();
}
maxDeque.offerLast(value);
queue.offerLast(value);
}
public int pop_front() {
if (queue.isEmpty()) {
return -1;
}
int value = queue.pollFirst();
// 只在真实出队项等于当前最大值时,同步删一份候选
if (value == maxDeque.peekFirst()) {
maxDeque.pollFirst();
}
return value;
}
}
type MaxQueue struct {
queue []int
maxDeque []int
}
func Constructor() MaxQueue {
return MaxQueue{}
}
func (this *MaxQueue) Max_value() int {
if len(this.maxDeque) == 0 {
return -1
}
return this.maxDeque[0]
}
func (this *MaxQueue) Push_back(value int) {
// 候选存值,严格较小才淘汰,相等最大值必须逐份保留
for len(this.maxDeque) > 0 && this.maxDeque[len(this.maxDeque)-1] < value {
this.maxDeque = this.maxDeque[:len(this.maxDeque)-1]
}
this.maxDeque = append(this.maxDeque, value)
this.queue = append(this.queue, value)
}
func (this *MaxQueue) Pop_front() int {
if len(this.queue) == 0 {
return -1
}
value := this.queue[0]
this.queue = this.queue[1:]
// 只在真实出队项等于当前最大值时,同步删一份候选
if value == this.maxDeque[0] {
this.maxDeque = this.maxDeque[1:]
}
return value
}
复杂度分析
- 时间复杂度:入队均摊 $O(1)$,出队和查询为 $O(1)$。单次入队最坏可能删除整个候选队列,但每个候选在全部操作中只进入和离开一次;底层动态数组扩容的开销也按均摊计算。
- 空间复杂度:$O(q)$,
q是操作期间普通队列的规模峰值。候选队列是尚未出队元素的一个子序列,长度不会超过普通队列。
关键点总结
[!green]
- 普通队列负责出队顺序,单调队列负责最大值候选,不能把被淘汰的候选也从普通队列删除。
- 后来的较大值更晚离开,能覆盖旧的较小值在剩余生命周期内的最大值作用。
- 存值的实现保留每份相等候选,出队时也只同步删除一份。
- 均摊 $O(1)$ 依靠每个候选至多入队、出队各一次,不代表每次入队只弹出一个元素。
易错点总结
[!yellow]
- 使用
<=清理候选尾部,会丢失重复值对应的份数,之后可能过早删除仍在队列中的最大值。- 普通队列每次出队都无条件删除候选队首,会把与当前出队元素无关的最大值误删。
- 只删除一个较小队尾而不用循环,可能留下破坏单调性的候选,队首也就不再保证是最大值。
- 空队列直接取队首,会越界或访问空值;应按约定返回
-1。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 239. 滑动窗口最大值 | 困难 | 同样用单调队列保留最大值候选,原题按固定窗口自动过期,本题由pop_front决定过期时机。 |
| 155. 最小栈 | 中等 | 两题都给容器增加极值查询,但队列删除最早元素,栈删除最新元素,辅助状态维护不同。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!