手把手带你实现JS循环队列

1,128 阅读7分钟

周末和小伙伴玩马里奥派对贼有意思,安利

正题

设计循环队列

设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

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

解析:

依题目,乍一看需要实现6个方法以及一个构造器。假设我们不考虑循环队列,那么应该如何实现以上6个方法和构造器呢?

依照题意,实现的无非就是 取首,取尾,插,删,判空,判满 而已,如果单纯通过数组去实现也是非常简单的。

  1. 构造器:

    构造器的入参为队列长度,那么我们可以定义队列长度变量(判满使用),以及队列实体Array

/**
 * @param {number} k
 */
var MyCircularQueue = function(k) {
    this.length = k //队列长度
    this.stack = [] // 队列
};
  1. 取首

    若队列为空,那么 return -1, 否则 return {数组第一个元素}

/**
 * @return {number}
 */
MyCircularQueue.prototype.Front = function() {
    if (this.stack.length === 0) {
        return -1
    } else {
        return this.stack[0]
    }
};
  1. 取尾

    若队列为空,那么 return -1, 否则 return {数组第最后元素}

/**
 * @return {number}
 */
MyCircularQueue.prototype.Rear = function() {
    return this.stack.length ? this.stack[this.stack.length - 1] : -1
};
  1. 插入

    若队列不满,则 push 并且 return true, 否则 return false

MyCircularQueue.prototype.enQueue = function(value) {
    if (this.stack.length < this.length) {
        this.stack.push(value)
        return true
    } else {
        return false
    }
};
  1. 删除

    若队列为空则 return false, 否则根据先进先出原则 unshift 弹出第一个元素, 并且 return true

if (this.stack.length > 0) {
        this.stack.shift()
        return true
    } else {
        return false
    }
  1. 判空
    return this.stack.length === 0
  1. 判满
    return this.stack.length === this.length

提交!

image.png

提交通过了,结果看起来很满意!但是这真的是实现了循环队列吗??真的就符合题意吗?

在回答这个问题之前,我们现了解一下循环队列!

什么是循环队列?

百度给出的解释: 为充分利用向量空间,克服"假溢出"现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。

可以很快提取出两个关键词,一是循环,而是队列(先进先出)。即使我们使用 push(进) shift(出)也仅满足了先进先出,并表现出循环的特点(首尾相接成环),所以以上的实现虽然通过了程序的判定,但实际上并不是循环队列的实现!

从实现构造器开始

构造器的入参是 队列长度,那么我们队列就应该持有队列长度变量,通常用 capacity 表示,其次需要实现首尾相接,也就必须要有首尾指针,分别为 frontrear,另外需要一个饿存放数据的空间,可用数组来代替,综上所述,那么我们的构造器雏形基本就有了。

/**
 * @param {number} k
 */
var MyCircularQueue = function(k) {
    this.front = 0
    this.rear = 0
    this.capacity = k
    this.queue = new Array(k)
};

如何判断空?

从简单到难,假定一个循环队列是空的,那么他会长什么样子呢?一图说明!

image.png

这是一个长度为7的队列,它里面什么都没有,那么我们带入构造函数中的变量看看

image.png

图中看出,首尾都在第0节点,长度为7的空队列。可以提取出以下几点信息:

  1. frontrear在同一节点
  2. front 节点为空

ok,那么判断循环列表为空的条件也就具备了

/**
 * @return {boolean}
 */
MyCircularQueue.prototype.isEmpty = function() {
    return this.rear === this.front && !this.queue[this.front]
};

插入队列是如何进行的?

在队列中插入元素,实际上可以理解为,队列尾指针 +1

1.gif

光看到这里我们会在实现队列插入的时候写出这样的代码:

/** 
 * @param {number} value
 * @return {boolean}
 */
MyCircularQueue.prototype.enQueue = function(value) {
    this.queue[this.rear] = value // 放入队列
    this.rear = this.rear + 1 // rear 指针右移
};

实际上是不够全面的,没有考虑到边界,即:当列表满时,会出现什么情况

所以,我们让插入继续走下去,我们继续插入:

1.gif

由于队列是循环的,那么我们可以看到当队列被塞满的时候, rear就会被重置到首位。如果按照上面的写法this.rear = this.rear + 1, rear 就会被一直增加,不仅超过了队列的最大长度,也没有实现真正的循环。那么应该去如何定义 rear 的表达式呢?

可以理解为,当 rear 达到7的时候,就会从1开始,像极了取余数,所以 this.rear = (this.rear + 1) % this.capacity,理解不过来的同学可以实际代入一下就可以理解了。

最终可将代码修改为:

/** 
 * @param {number} value
 * @return {boolean}
 */
MyCircularQueue.prototype.enQueue = function(value) {
    if (this.isFull()) {
        return false // 队列满了
    } else {
        this.queue[this.rear] = value
        this.rear = (this.rear + 1) % this.capacity
        return true
    }
};

如何判断队列满了呢?

在添加队列的同时,有限制条件,即:队列满时,队列将不会被添加。并且 return false,那么重要的条件就是如何判断队列满了呢?那么接下来就实现 isFull()方法。

其实在上图中已经表现出:

image.png

不难发现, rear指针回到了首位,并且和 front 重合。所以有

this.rear === this.front

但是需要区分空队列的情况:

image.png

这种情况也是 this.rear === this.front,唯一不同的就是队列中没有任何元素,所以我们要判断是否是因为队列满了而达到重复的情况,可以得到:

  1. this.rear === this.front
  2. !!this.queue[this.front]

实现 isFull()

/**
 * @return {boolean}
 */
MyCircularQueue.prototype.isFull = function() {
    return this.rear === this.front && !!this.queue[this.front]
};

开始删除吧

增加实现完了,开始实现删除。首先了解删除节点的操作:

1.gif

由上图可知删除节点的操作仅仅对 front进行操作,即

  1. 删除 front 指针对应的元素
  2. front 指针右移
  3. 当队列为空时,不进行删除操作 return false ps: 考虑边界问题, front右移应采用 取余数的方法,表达循环链表的特点

实现:

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

取出首元素

取出首元素,即找到 front 指针所对应的节点

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

仅仅要考虑如果队列为空则 return -1 即可

取出尾元素

这个相对首元素来说稍微复杂一点,因为 rear 指向的是尾元素下一个节点的指针,所以应该取 rear - 1指针所对应的节点。另外,考虑 rear 是循环过来的第一个节点,那么就取出最后一个节点即可

/**
 * @return {number}
 */
MyCircularQueue.prototype.Rear = function() {
    let rearIndex = this.rear === 0 ? this.capacity - 1 : this.rear - 1
    return this.isEmpty() ? -1 : this.queue[rearIndex]
};

至此,上述应题要求的所有方法均已实现,提交!

image.png

结果是通过的,并且也是完全实现了循环队列的所有特性!这样才是应题的解答!

完整代码:

/**
 * @param {number} k
 */
var MyCircularQueue = function(k) {
    this.front = 0
    this.rear = 0
    this.capacity = k
    this.queue = new Array(k)
};

/** 
 * @param {number} value
 * @return {boolean}
 */
MyCircularQueue.prototype.enQueue = function(value) {
    if (this.isFull()) {
        return false
    } else {
        this.queue[this.rear] = value
        this.rear = (this.rear + 1) % this.capacity
        return true
    }
};

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

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

/**
 * @return {number}
 */
MyCircularQueue.prototype.Rear = function() {
    let rearIndex = this.rear === 0 ? this.capacity - 1 : this.rear - 1
    return this.isEmpty() ? -1 : this.queue[rearIndex]
};

/**
 * @return {boolean}
 */
MyCircularQueue.prototype.isEmpty = function() {
    return this.rear === this.front && !this.queue[this.front]
};

/**
 * @return {boolean}
 */
MyCircularQueue.prototype.isFull = function() {
    return this.rear === this.front && !!this.queue[this.front]
};

/**
 * Your MyCircularQueue object will be instantiated and called as such:
 * var obj = new MyCircularQueue(k)
 * var param_1 = obj.enQueue(value)
 * var param_2 = obj.deQueue()
 * var param_3 = obj.Front()
 * var param_4 = obj.Rear()
 * var param_5 = obj.isEmpty()
 * var param_6 = obj.isFull()
 */

通过这次的题解,是否更进一步了解了设么是循环队列了呢?循环队列的优点也可以一目了然了,空间还是队列的最大空间,但是可以实现增删取,达到了空间利用的最大化。

那么什么事百度词条中解释的: 假溢出

通过增加队列节点,可以看出,当队列还没有满时,但rear指针可能已经到了最后节点,那么 rear 节点右移可能会导致溢出,但实际上因为循环队列的特性,rear节点会重新回到首个节点的位置,并不会造成溢出。

1.gif