Bootstrap

设计循环队列

 

目录

循环队列的定义

需要实现的操作

 循环队列设计

底层结构确定

整体思路

操作功能的实现 

初始化

检查循环队列是否已满

检查循环队列是否为空

插入 

 删除

 获取队首元素

获取队尾元素 

销毁

循环队列代码展示

 总结


循环队列的定义

循环队列是一种线性数据结构,其操作表现基于 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;
}

 总结

循环队列的设计改变了以往我对队列底层结构总是为链表的认知,也让我感受到了以数组作为底层结构时的巧妙设计。这次设计最主要的部分就是在判断队列已满和队列为空的条件,以及多开辟一个单位的占位符。

;