队列是一种「先进先出」的数据结构,而循环队列是一种首尾相连的队列。题目要求设计一个循环队列。
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.
Your implementation should support following operations:
MyCircularQueue(k): Constructor, set the size of the queue to be k.
Front: Get the front item from the queue. If the queue is empty, return -1.
Rear: Get the last item from the queue. If the queue is empty, return -1.
enQueue(value): Insert an element into the circular queue. Return true if the operation is successful.
deQueue(): Delete an element from the circular queue. Return true if the operation is successful.
isEmpty(): Checks whether the circular queue is empty or not.
isFull(): Checks whether the circular queue is full or not.
分析
在做这道题之前,需要知道队列和循环队列的定义。你可以把队列想象成排队买早餐,先排队的同学可以先买到早餐,然后出队。如图:
对于队列的设计,一般使用动态数组来实现。实现接口如下:
class ArrayQueue {
public:
void enqueue(E); // O(1)
E dequeue(); // O(n)
E getFront(); // O(1)
int getSize(); // O(1)
boolean isEmpty();// O(1)
};
使用动态数组实现,在出队的时候,时间复杂度为 O(n),因为出队后,数组中的元素需要移动位置。为了使出队时间复杂度为 O(1),就出现了循环队列,它不需要移动位置,需要 front 和 tail 分别指向队列的头和尾。如下图:
初始化时 front 和 tail 都指向队列的第一个位置,容量 capacity 为 6:
入队时需要移动 tail 的位置:
出队时需要移动 front 的位置:
分析完循环队列后,我们看看题目要求实现的接口:
class MyCircularQueue {
public:
// 初始化,大小为 k
MyCircularQueue(int k) {}
// 入队
bool enQueue(int value) {}
// 出队
bool deQueue() {}
// 队首元素
int Front() {}
// 队尾元素
int Rear() {}
// 是否为空
bool isEmpty() {}
// 是否满了
bool isFull() {}
};
在 C++ 中 Array 的大小一旦声明是不可改变的,它的可变版本是向量 vector。如果队列设计时已经确认了大小,可以使用 Array 和 Vector,这里我们使用 vector 来实现循环队列。通过接口可以知道,初始化的时有一个参数 k 标明了循环队列的大小。
队列是否为空可以使用 front == tail 来判断,但是 front == tail 也可能是队满的时候,代码中使用了一个临时变量来记录队列是否为空,也可以浪费一个存储空间,使用 (tail+1)%N=front 判断队满。
C++代码
代码来自 LeetCode:
class MyCircularQueue {
// 使用向量来表示队列中的元素
vector<int> q;
// 头、尾、队列大小
int head, tail, N;
// 队列是否为空
bool empty;
public:
// 初始化,大小为 k
MyCircularQueue(int k) {
// 初始化时,头和尾都指向队列的第一个位置
head = 0;
tail = 0;
N = k;
q = vector<int> (k);
empty = true;
}
// 入队
bool enQueue(int value) {
if (isFull()) return false;
empty = false;
q[tail] = value;
// 移动尾指针
tail = (tail+1) % N;
return true;
}
// 出队
bool deQueue() {
if (isEmpty()) return false;
q[head] = 0;
head = (head+1) % N;
// 头和尾相等时判断为空
if (head == tail) empty = true;
return true;
}
// 头
int Front() {
if (isEmpty()) return -1;
return q[head];
}
// 尾
int Rear() {
if (isEmpty()) return -1;
return q[(tail-1+N)%N];
}
// 是否为空,使用一个临时变量来记录队列是否为空
bool isEmpty() {
return empty;
}
// 是否已满
bool isFull() {
return (empty) ? false : head == tail;
}
};
总结
本道题目由于初始时告诉了容量,使得题目变得简单了。如果容量不确定,必须考虑到扩容和缩容处理,不过可以使用类似 C++ 一样的向量 vector 来实现,它自身支持扩容。而本题完全可以使用 Array 来实现。本题的一个关键点是 「 环 」,涉及到计算 front 和 tail 的值。本文的目的不是为了说明这道题这么做,而是引出队列的特性和循环队列为何出现,来起到学习算法的同时也学懂了数据结构。
推荐阅读:
图解算法