队列

66 阅读5分钟

粗浅理解

队列是一种有序的线性表, 并且只能在两头操作。
所谓只能在两头操作就是指,一个数组只能使用push 和 shift , 如果是双端队列还能使用 unshift和pop.

出队和入队

队列有两种操作,入队和出队,在js数组中就是 push和shift。就和日常生活中排队打饭一样。只能从队首出队, 队尾入队。因此,说起队列,就会想到先进先出。

一个队列通常会有两个指针(暂且理解为数组下标),队首和队尾。

  • 入队,元素从队尾入队, 入队之后,队尾指针往后移一位。
  • 出队,元素从队首出队, 出队之后,队首指针往后移一位。

逻辑上的队列的出队操作,和实际生活中的不太一样。 数据结构中的队列在队首出队(unshift)之后,并不会让整个队伍往前挪,因为每个元素一小步,整个队列就是n-1步。这样删除队首的操作, 开销就大了。

因此,在出队操作之后,我们选择,让队首指针往后走一步

队满

和js普通的Array 不同, 一般的队列都是有容量限制的,类似定长数组FloatArray. 队列满了之后必须让队首出队,腾出位置给后来的。但是, 队首出队真的能腾出空间吗?

上面说了,出队之后,只会把队首指针往后移,这样前面的空间就空着了。前面的空间会空闲,是因为队列的存储空间是在创建队列之前就申请好了,固定了的。不同于我们js的动态数组。
这样就产生了一个问题, 那就是伪队满。因为队列只能从队尾入队, 那么不管前面是否有空位,只要最后一个位置被占了,就不能入队了。

循环队列

为了解决这个问题, 有人想出了循环队列,也就是让队列的的最后一个位置的下一位是队列的第一个位置。就像蛇咬住自己的尾巴。这种操作是用取余运算符来实现的。 下面直接上代码。

就是力扣的设计循环队列

MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/de… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

MyCircularQueue.prototype.enQueue = function(value) {
    if(this.isFull()){ return false}

    this.tail = (this.tail+1) % this.size
    this.queue[this.tail] = value ;
    this.count++ ;
        return true 
    
};

/**
 * @return {boolean}
 */
MyCircularQueue.prototype.deQueue = function() {
if(this.isEmpty()) return false ;
this.queue[this.head] = null ;
this.head = (this.head +1) % this.size ;
this.count--
return true ;
};

/**
 * @return {number}
 */
MyCircularQueue.prototype.Front = function() {
if(this.isEmpty()) return -1 ;
return this.queue[this.head]
};

/**
 * @return {number}
 */
MyCircularQueue.prototype.Rear = function() {
if(this.isEmpty()) return -1 ;
return this.queue[this.tail]

};

/**
 * @return {boolean}
 */
MyCircularQueue.prototype.isEmpty = function() {
    return this.count === 0 
};

/**
 * @return {boolean}
 */
MyCircularQueue.prototype.isFull = function() {
    return this.size === this.count ; 

};

队列的应用,常用的任务队列,消息队列。符合先来后到的执行、处理的, 都用到了队列的思想。

单调队列

上面已经说了队列的基本性质,先进先出。单调队列, 顾名思义就是有单调性的队列。单调递增或者单调递减。

为了维护这个单调性,这种队列在入队时会有一些操作。大概的流程如下,还是结合实际场景题目来说。

题目场景就是的有一个Number数组arr. 然后固定一个容量为k的容器在数组上滑动。
也就是使用数组方法 slice截取k个元素 arr.slice(i, i+k) i0开始, 直到i+K === length。
对于每个i 都要输出容器内的最小值。

单调栈的维护, 这里以单调递增队列为例。 单调递增队列, 队首就是这个区间的最小值

  • 首先, 新元素入队。 如果新元素比队尾元素小,直接入队的话显然就破坏了单调性。因此,新元素较小时, 就砍掉队尾元素,让队尾元素出队, 直到新元素不小于队尾元素或者队空。 这样就维护了单调性
  • 入队之后,如果入队过的元素数量小于容量k,继续入队。直到历史记录大于等于k. 这时,再来判断我们的队首元素是否超出[i,i+k -1]的范围 ,超出则队首出队。这里是为了维护数据的有效性,

一个小重点

这里队列里面存储的是索引,而不是元素的值。因为后面要判断索引是否超出有效范围。

这就是存储原始数据。所谓原始数据,就是能拿到全部信息,是数据的源头。对于数组元素来说,原始信息就是数组加下标。 通过索引可以轻易的取得数据,但是通过数据反过来想要取得索引就费劲了, 而且还不一定能取得。

代码

var maxSlidingWindow = function(nums, k) {
    let queue = [], ret = [];
    for(let i = 0; i < nums.length; i++){
        const el  = nums[i] 
        while( queue.length && nums[queue[queue.length -1]] < el){
            queue.pop()
        }
        /* 注意undefined 和任何数进行 比较结果是false 所以这里还是避免 */
            queue.push(i)
        if( queue[0] <= i -k){ queue.shift()}
            /* 已遍历元素不足k时 直接下一轮*/

            if(i <k -1){ continue}
         ret.push(nums[queue[0]])

    }
    return ret;
};

下面直接上力扣题解。

239. 滑动窗口最大值(leetcode-cn.com/problems/sl…)

知道了单调队列这种数据结构,这题肯定算不上困难,我就不说它及其简单了