前端算法第一三五弹-队列的最大值

153 阅读3分钟

“Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。”

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_valuepush_backpop_front均摊时间复杂度都是O(1)。

若队列为空,pop_frontmax_value 需要返回 -1

示例 1:

输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]

示例 2:

输入:
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]

限制:

  • 1 <= push_back,pop_front,max_value 的总操作数 <= 10000
  • 1 <= value <= 10^5

单调递减的双端队列

  • 维护一个始终递减的队列,这样 max_value 的时间复杂度就优化到了 O(1),直接取第一位即可

  • 如何维护? 我们只需要在插入每一个元素 value 时,从队列尾部依次取出比当前元素 value 小的元素,直到遇到一个比当前元素大的元素 value' 即可。

  • pop_front 方面

    • 如果原队列要弹出的元素正好是单调队列中的队头,那也要弹出
    • 如果弹出的元素不是单调队列头部,那不用管了,因为 max_value 的 API 只跟队头有关,只要队头元素不要在已经被移出原队列的基础上,还存在单调队列里就行

上面的过程保证了只要在元素 value 被插入之前队列递减,那么在 value 被插入之后队列依然递减。

而队列的初始状态(空队列)符合单调递减的定义。

由数学归纳法可知队列将会始终保持单调递减。

var MaxQueue = function() {
    this.queue = []; // 用一个正常数组存取每次 push 进来的元素
    this.maxQueue = []; // 该队列专门为了简化 max_value 这个 API,称它为单调递减队列
};

/**
 * @return {number}
 */
MaxQueue.prototype.max_value = function() {
    return this.maxQueue.length ? this.maxQueue[0] : -1;
};

/** 
 * @param {number} value
 * @return {void}
 */
MaxQueue.prototype.push_back = function(value) {
    this.queue.push(value); // 正常压入该队列,然后下面维护另一个单调递减队列
    console.log(this.queue);
    // 队尾开始遍历,遇到值比 value 小的就弹出,维护单调递减队列
    while(this.maxQueue.length && this.maxQueue[this.maxQueue.length - 1] < value) {
        this.maxQueue.pop();
    }
    this.maxQueue.push(value);
    console.log(this.maxQueue);
};

/**
 * @return {number}
 */
MaxQueue.prototype.pop_front = function() {
    if(!this.queue.length) return -1;

    const val = this.queue.shift(); // 该队列由于是正常存放所有压入数组的队列,正常弹出即可
    // 如果弹出的元素正好是单调队列中的队头,那也要弹出,如果弹出的元素不是单调队列头部,那不用管了hh,因为 max_value 的API 只跟队头有关,只要队头元素不要在已经被移出原队列的基础上,还存在单调队列里就行
    if(this.maxQueue[0] === val) {
        this.maxQueue.shift();
    }

    return val;
};

/**
 * Your MaxQueue object will be instantiated and called as such:
 * var obj = new MaxQueue()
 * var param_1 = obj.max_value()
 * obj.push_back(value)
 * var param_3 = obj.pop_front()
 */

复杂度

  • 时间复杂度:O(1)(插入,删除,求最大值)

    • 删除操作于求最大值操作显然只需要 O(1) 的时间。
    • 而插入操作虽然看起来有循环,做一个插入操作时最多可能会有 n 次出队操作。但要注意,由于每个数字只会出队一次,因此对于所有的 n 个数字的插入过程,对应的所有出队操作也不会大于 n 次。因此将出队的时间均摊到每个插入操作上,时间复杂度为 O(1)。
  • 空间复杂度:O(n),需要用队列存储所有插入的元素。