[路飞]_每天刷leetcode_54( 设计循环队列 Design Circular Queue)

157 阅读4分钟

「这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战

设计循环队列 Design Circular Queue

LeetCode传送门622. 设计循环队列

题目

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

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

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

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

Design your implementation of the circular queue. The circular queue is a linear data structure in which the operations are performed based on FIFO (First In First Out) principle and the last position is connected back to the first position to make a circle. It is also called "Ring Buffer".

One of the benefits of the circular queue is that we can make use of the spaces in front of the queue. In a normal queue, once the queue becomes full, we cannot insert the next element even if there is a space in front of the queue. But using the circular queue, we can use the space to store new values.

Implementation the MyCircularQueue class:

  • MyCircularQueue(k) Initializes the object with the size of the queue to be k.

  • int Front() Gets the front item from the queue. If the queue is empty, return -1.

  • int Rear() Gets the last item from the queue. If the queue is empty, return -1.

  • boolean enQueue(int value) Inserts an element into the circular queue. Return true if the operation is successful.

  • boolean deQueue() Deletes an element from the circular queue. Return true if the operation is successful.

  • boolean isEmpty() Checks whether the circular queue is empty or not.

  • boolean isFull() Checks whether the circular queue is full or not.

You must solve the problem without using the built-in queue data structure in your programming language.

Example:


Input
["MyCircularQueue", "enQueue", "enQueue", "enQueue", "enQueue", "Rear", "isFull", "deQueue", "enQueue", "Rear"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
Output
[null, true, true, true, false, 3, true, true, true, 4]

Explanation
MyCircularQueue myCircularQueue = new MyCircularQueue(3);
myCircularQueue.enQueue(1); // return True
myCircularQueue.enQueue(2); // return True
myCircularQueue.enQueue(3); // return True
myCircularQueue.enQueue(4); // return False
myCircularQueue.Rear();     // return 3
myCircularQueue.isFull();   // return True
myCircularQueue.deQueue();  // return True
myCircularQueue.enQueue(4); // return True
myCircularQueue.Rear();     // return 4

Constraints:

  • 1 <= k <= 1000
  • 0 <= value <= 1000
  • At most 3000 calls will be made to enQueue, deQueue, Front, Rear, isEmpty, and isFull.

思考线


解题思路

这道题要我们设计一个循环队列的类,要实现循环队列,我们需要实现其中的每一个方法。

我们可以在 MyCircularQueue中设置一些属性来保存我们的队列,我们知道需要实现Front\Rear\enQueue\deQueue\isEmpty\isFull这些方法。于是我们可以设置4个变量来记录我们的队列

  • queue: 这个保存我们的队列
  • head: 这个保存队列的头部
  • size: 保存队列的长度
  • len: 保存队列的最大长度

而我们在构造类的时候,让队列queue初始化,同时初始化head = 0, size =0, len = k

而在我们的所有方法中isEmptyisFull比较容易实现我们只需要判断size的值是否是0或者len即可。

而接下来比较容易做的是获取到头部Front,我们只需要判断

  • 若队列是空的,返回 -1
  • 若队列不为空,返回head下标对应的值

既然实现了Front,那么Rear也是一样的。我们只要判定队列不为空的情况下,找到尾部的下标即可。

而如何实现这个下标的查找呢?

我们知道在不超过len的情况下,尾部的下标为head + size -1.

我们可以分为两种情况

  1. (head + size -1)< len的情况,我们的下标就是head +size -1
  2. (head + size -1)>= len的情况, 我们可以余上len,这样就能得到正确的下标值了。

而我们知道,任意小于len的数被len求余,结果还是其本身,所以,我们就可以把两种情况合二为一变为:

lastInd = (head + size -1)%len

接下来我们再考虑deQueue这个函数如何实现。

我们要从队列中删除一个元素,也就是从头部删除。我们可以通过让head++的方式直接把头部移到下一位即可。在这里值得注意的是,如果此时的head === len -1说明我们已经到了数组的尾部,我们此时需要把head指向下标为0的位置。

最后我们实现一下enQueue方法

这个方法和Rear方法类似, 我最开始思路是,只是若找到的lastInd若和len -1相等,说明到达了最后,我们下一个元素要添加到数组下标为0的位置。否则我们只需要在下标为lastInd + 1的位置放入我们的值即可。

    enQueue(value: number): boolean {
        if (this.isFull()) {
            return false
        }
        if ((this.head + this.size - 1) % (this.len) !== this.len - 1) {
            this.queue[(this.head + this.size - 1) % (this.len) + 1] = value;
        } else {
            this.queue[0] = value;
        }
        this.size++
        return true;
    }

而后面再看这道题的时候,我发现我想的有点多余了,其实这一步我们只要在 head + size -1的基础上 执行+1操作,然后余上len就可以得到对应的放入元素的下标。

通过以上的分析,我们知道了每一个方法的实现想法

最后附上代码如下:

class MyCircularQueue {
    queue: number[] // 队列
    head: number // 头部位置的数组下标
    size: number // 队列的长度
    len: number; // 队列能放置的最大长度
    constructor(k: number) {
        this.queue = new Array(k);
        this.head = 0;
        this.size = 0;
        this.len = k;
    }

    enQueue(value: number): boolean {
        if (this.isFull()) {
            return false
        }
      	// 找到最后的位置并+1,得到插入位置
        this.queue[(this.head + this.size) % (this.len)] = value;
        this.size++
        return true;
    }

    deQueue(): boolean {
        if (this.isEmpty()) {
            return false
        }
        if (this.head === this.len - 1) { // 分类讨论 head的位置
            this.head = 0;
        } else {
            this.head++;
        }
        this.size--
        return true;
    }

    Front(): number {
        if (this.isEmpty()) return -1;
        return this.queue[this.head];
    }

    Rear(): number {
        if (this.isEmpty()) return -1;
      	// 因为是实现循环队列,所以我们对结果进行以 len 为基数取模
        return this.queue[(this.head + this.size - 1 + this.len)% (this.len)];
    }

    isEmpty(): boolean {
        return this.size === 0;
    }

    isFull(): boolean {
        return this.size === this.len;
    }
}

/**
 * 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()
 */

时间复杂度 O(1): 在上面的数据结构中,所有的方法都有恒定的时间复杂度。

这就是我对本题的解法,如果有疑问或者更好的解答方式,欢迎留言互动。