目录
循环队列的定义
循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
需要实现的操作
MyCircularQueue(k)
: 构造器,设置队列长度为 k 。Front
: 从队首获取元素。如果队列为空,返回 -1 。Rear
: 获取队尾元素。如果队列为空,返回 -1 。enQueue(value)
: 向循环队列插入一个元素。如果成功插入则返回真。deQueue()
: 从循环队列中删除一个元素。如果成功删除则返回真。isEmpty()
: 检查循环队列是否为空。isFull()
: 检查循环队列是否已满。
MyCircularQueue* myCircularQueueCreate(int k)//初始化
bool myCircularQueueIsFull(MyCircularQueue* obj)//检查循环队列是否已满
bool myCircularQueueIsEmpty(MyCircularQueue* obj)//检查循环队列是否为空
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)//插入元素,如果成功返回真
bool myCircularQueueDeQueue(MyCircularQueue* obj)//删除元素,如果成功返回真
int myCircularQueueFront(MyCircularQueue* obj)//获取队首元素
int myCircularQueueRear(MyCircularQueue* obj)//获取队尾元素
void myCircularQueueFree(MyCircularQueue* obj)//销毁
循环队列设计
底层结构确定
在设计循环队列时,我们需要确定循环队列的底层结构。
当我们使用链表作为底层结构设计循环队列时,对于插入元素、删除元素的操作我们就需要通过改变每一个结点的指向来实现,十分繁琐;在对循环队列是否为空还是满的判断上也有较大的难度去实现。相比之下,数组不仅拥有固定的空间大小,其本身在存储上也是连续的,方便了我们插入与删除的操作。
所以在设计循环队列的时候,将数组作为底层结构具有更大的便捷性。
整体思路
根据操作功能实现的需要,我们实现插入、与删除的功能,同时又能够找到队头与队尾。
所以我们需要设计头部位置(front)和尾部位置(rear)。依照队列队尾入数据,队头出数据的原则来进行操作。
起始,队列为空,rear和front需要在同一位置,然后根据rear来进行元素的插入工作,插入一个元素,rear++,直到rear完成一次循环。
如图所示,当rear继续往后走时,因为循环队列已满无法再继续插入数据。因为循环队列的特性,rear需要回到front的位置。
在进行删除元素的操作时,我们根据队列的特性,队头出数据,可知我们需要利用front来进行数据删除操作,和rear相似,删除一个元素,front++,直到front完成一次循环。
如图所示,当front继续往后走时,因为循环队列为空无法再继续删除数据。因为循环队列的特性,front需要回到起始的位置。
由此可见,front和rear在走完一次循环后都需要回到起始位置,但是纯粹的++无法使它们回到下标为0的位置,同时触发回溯的条件也需要进行寻找。
当rear回到下标0的位置时,front正好处于下标0的位置,由此是否可得当rear == front时,队列已满?进而触发下一轮的循环?
但是当我们回看循环队列最初始的状态时,rear就已经和front相等,rear == front包含了两种含义:1.队列已满。2.队列为空。所以将rear == front作为触发第二次循环的条件是错误的。
为了达到循环正确被完成和一次又一次触发的目的,我们需要将原本的数组扩大一个单位的空间,但是其中不存储任何数值,也不会进行任何插入与删除操作,只是将其作为占位符来完成判断队列已满和队列为空。
由此根据上图下标关系可得,当(rear+1)%(k+1)== front时,可以表示队列已满。
队列为空的条件也可由rear == front单独表示。
操作功能的实现
已知我们决定将数组作为循环队列的底层结构,第一步就是将队列结构进行设计。
typedef struct {
int* arr;//使用动态结构更具灵活性,方便我们调整数组大小
int front;//确定头部位置
int rear;//确定尾部位置
int capacity;//计算数组大小
} MyCircularQueue;
初始化
循环队列的设计做好准备工作,为队列和数组开辟足够的空间大小。
//初始化
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* pst = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
pst->arr = (int*)malloc(sizeof(int)*(k+1));//多开辟一个单位的空间,保证循环的进行
pst->front = pst->rear = 0;
pst->capacity = k;//实际可用空间大小
return pst;
}
检查循环队列是否已满
根据整体思路得出该函数结论
//检查循环队列是否已满
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->rear+1) % (obj->capacity+1) == obj->front;
}
检查循环队列是否为空
根据整体思路得出该函数结论
//检查循环丢列是否为空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->rear == obj->front;
}
插入
首先需要考虑,如果循环队列已满,则无法继续插入数据。
如果可以继续插入数据,则需要根据rear的位置进行插入,直到rear无法继续向后需要开启新一轮循环为止。
//成功插入返回真
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(myCircularQueueIsFull(obj))//当循环队列已满时
{
return false;
}
obj->arr[obj->rear++] = value;
obj->rear %= (obj->capacity+1);//无法继续向后,需要开启新一轮的循环
return true;
}
删除
首先需要考虑,如果循环队列为空,则无法继续删除数据。
如果可以继续删除数据,则需要根据front的位置进行删除,直到front无法继续向后需要开启新一轮循环为止。
//如果成功删除就返回真
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))//当循环队列为空时
{
return false;
}
obj->front++;
obj->front %= obj->capacity+1;//无法继续向后,需要开启新一轮的循环
return true;
}
获取队首元素
当循环队列为空时,无法获取队首,返回-1。
当循环队列不为空时,返回队首。
//获取队首元素
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))//判断循环队列是否为空
{
return -1;
}
return obj->arr[obj->front];
}
获取队尾元素
当循环队列为空时,无法获取队尾,返回-1。
当rear在队尾时,需要rear-1来找到真正存储数据的有效位置。但是当rear在队头时,rear-1无法获取,那么就需要另设变量prev来找到队尾元素,因为capacity存储数组有效数据个数,所以prev可以通过获取capacity个数来确定队尾位置。
//获取队尾元素
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
int prev = obj->rear-1;
if(obj->rear == 0)
{
prev = obj->capacity;//当rear在队头时需要根据capacity个数来确定队尾
}
return obj->arr[prev];
}
销毁
//销毁循环队列
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->arr);
free(obj);
obj = NULL;
}
循环队列代码展示
typedef struct {
int* arr;
int front;
int rear;
int capacity;
} MyCircularQueue;
//初始化
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* pst = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
pst->arr = (int*)malloc(sizeof(int)*(k+1));
pst->front = pst->rear = 0;
pst->capacity = k;
return pst;
}
//检查循环队列是否已满
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->rear+1) % (obj->capacity+1) == obj->front;
}
//检查循环丢列是否为空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->rear == obj->front;
}
//成功插入返回真
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(myCircularQueueIsFull(obj))
{
return false;
}
obj->arr[obj->rear++] = value;
obj->rear %= (obj->capacity+1);
return true;
}
//如果成功删除就返回真
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
{
return false;
}
obj->front++;
obj->front %= obj->capacity+1;
return true;
}
//获取队首元素
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
return obj->arr[obj->front];
}
//获取队尾元素
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
int prev = obj->rear-1;
if(obj->rear == 0)
{
prev = obj->capacity;
}
return obj->arr[prev];
}
//销毁循环队列
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->arr);
free(obj);
obj = NULL;
}
总结
循环队列的设计改变了以往我对队列底层结构总是为链表的认知,也让我感受到了以数组作为底层结构时的巧妙设计。这次设计最主要的部分就是在判断队列已满和队列为空的条件,以及多开辟一个单位的占位符。