在之前的文章中,我们介绍了栈的基本概念与用法,栈是遵循先进后出的原则,而今天我们要介绍的队列,则是栈的相反概念,即先进先出,最后将具体介绍环形队列问题,希望这篇文章可以帮助到你。
目录
7. 检测队列是否为空,如果为空返回非零结果,如果非空返回0
队列的概念及结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头
队列在现实生活中应用广泛,如在银行的抽号机,先来的人会得到一个号码,然后在办理业务的时候,按照号码进行叫人,完成一个业务就继续往下实现。
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
数组在数组头上出数据时,必须让后面的元素依次向前挪动,因此效率不如链表。
我们建立队列的链表节点,如下
队列的链表节点
typedef struct QListNode
{
struct QListNode* _pNext;
QDataType _data;
}QNode;
而如果我们要实现队列的入队和出队,则需要找到队列的首和尾,如果每次都用遍历链表的方法来寻找尾节点,那么效率就比较低了,所以我们需要在函数中传入三个变量
(头节点,尾节点,数据)
那我们就可以想到将头节点和尾节点封装为一个结构体,这样只需要传入结构体就能代替两个变量,让函数参数更加简洁
由于我们之后还要实现队列元素的个数的函数,所以不妨我们再在结构体中加上size变量来记录队列元素的个数,之后实现函数也更加方便
因此我们创建队列的结构
队列的结构
typedef struct Queue
{
QNode* _front;
QNode* _rear;
int size;
}Queue;
下面我们就来实现队列的基本函数
队列函数
// 初始化队列
void QueueInit(Queue* q);
// 队尾入队列
void QueuePush(Queue* q, QDataType data);
// 队头出队列
void QueuePop(Queue* q);
// 获取队列头部元素
QDataType QueueFront(Queue* q);
// 获取队列队尾元素
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
int QueueEmpty(Queue* q);
// 销毁队列
void QueueDestroy(Queue* q);
队列函数和栈函数的用法大差不差,因此我们只需要效仿栈的函数实现即可
1.初始化队列
初始化队列,我们仍然只需要将对应的变量置为对应的空值即可
代码如下
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
2.队尾入队列
入队列即插入数据,对应链表的知识就是尾插的实现,因此我们需要用malloc开辟新节点,注意要检查是否开辟成功,若不成功就及时的中断代码。
插入的时候分为两种情况,第一种队列为空,即head为空时,那么就需要将head和tail都指向新节点newnode,另一种即正常的情况,我们只需要将新节点插入到tail的后面,再将tail向后移即可,不要忘记将size++;
这就是我们之前队列结构设计的好处,有head和tail以及size就省去了许多冗杂的代码,可以清晰的实现
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->val = data;
newnode->next = NULL;
if (q->phead == NULL)
{
q->phead = q->ptail = newnode;
}
else
{
q->ptail->next = newnode;
q->ptail = newnode;
}
q->size++;
}
3. 队头出队列
出队列即头删的实现,我们首先需要用断言判断队列是否为空,若为空就中断代码的运行,然后将出队列也分为两种情况
第一种情况,只有以一个数据,即head的next为空,这时tail = head,所以我们在free头节点的之后,要记者把tail也置为空,否则会出现野指针的使用
第二种情况,即正常的情况,由于head节点需要释放,需要记录head的下一个位置,只需要新建一个next变量赋值为head的next ,然后free头节点head,再将next作为新的头即可
最后不要忘了将size--;
代码如下:
void QueuePop(Queue* q)
{
assert(q);
assert(q->size != 0);
if (q->phead->next == NULL)
{
free(q->phead);
q->ptail = q->phead = NULL;
}
else
{
QNode* next = q->phead->next;
free(q->phead);
q->phead = next;
}
q->size--;
}
4.获取队列头部元素
和获得栈顶元素类似,先确保head不为空,再返回值即可,不作过多解释
代码如下:
QDataType QueueFront(Queue* q)
{
assert(q);
assert(q->phead);
return q->phead->val;
}
5.获得队列队尾元素
这个函数的实现,就得益于之前队列结构体的建立,有tail变量,可以轻松的获得尾节点,
如果没有设置tail变量就需要依次遍历链表来实现了
代码如下:
QDataType QueueBack(Queue* q)
{
assert(q);
assert(q->phead);
return q->ptail->val;
}
6.获得队列有效个数
直接return size
代码如下:
int QueueSize(Queue* q)
{
assert(q);
return q->size;
}
7. 检测队列是否为空,如果为空返回非零结果,如果非空返回0
用size==0来判断队列是否为空
代码如下:
bool QueueEmpty(Queue* q)
{
assert(q);
return q->size == 0;
}
8.销毁队列
销毁队列和单链表的销毁类似,建立cur节点从头节点开始遍历,然后依次释放开辟的节点空间,完成队列的销毁,最后将队列元素各值置空即可。
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
q->phead = NULL;
q->ptail = NULL;
q->size = 0;
}
以上即队列基本函数的实现,逻辑和代码上都比较简单,希望大家能够掌握
下面我们拓展一下环形队列问题
环形队列问题
题目:
下面是要实现的函数接口
typedef struct {
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
}
int myCircularQueueFront(MyCircularQueue* obj) {
}
int myCircularQueueRear(MyCircularQueue* obj) {
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
}
void myCircularQueueFree(MyCircularQueue* obj) {
}
思路:
这个题的意思是,空间大小是固定的,在有限的空间种保证先进先出
那么这道题我们就有两种选择,一种是用链表,一种是用数组
我们分析一下题目可知,由于环形队列是循环的,即最后一个元素的下一个位置就是开头的位置,那么我们要么用双向链表,要么用数组,这样实现是更为合理的,而数组更为方便,能够直接用下标进行访问,所以我们选择数组实现,具体结构如图所示
我们设置head,tail来记录头节点和尾节点的位置
当push数据的时候就push到tail节点的位置,然后让tail+1,到末尾的时候就返回下标0的位置重新开始,效果如图
pop的时候也是一样的,pop删除head位置的数据,然后令head++,走到尾的位置时,就返回下标尾0的位置重新开始
结构如图所示
如果上面的思路能够理解的话,那这时候我们需要关注一个问题,什么时候是环形队列满的状态,什么时候是环形链表空的状态呢?
我们发现当head == tail的时候,既是满也是空,那么问题就出现了,我们不可能让同一个条件来表示两种状态,这样会混淆的,那我们该怎么办呢?
我们有两种解决方法
1.在结构体中加入一个size变量,来判断为空还是为满
2.额外多开辟一个空间
我们这里用第二个方法来解决问题
如图所示
当head == tail则为空
tail+1 = head或者当tail = k&&head == 0时为满
或者利用(tail+1)%(k+1)==head 来判断为满
这个公式需要结合图好好理解一下,非常的神奇
解题:
思路明确我们来实现一下题目代码
先建立结构体,头head,尾tail,空间a,空间大小k
typedef struct {
int* a;
int head;
int tail;
int k;
} MyCircularQueue;
初始化环形队列
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
obj->a = (int*)malloc(sizeof(int)*(k+1));
obj->head = 0;
obj->tail = 0;
obj->k = k;
return obj;
}
判断是否为空
按照我们上面的思路head==tail时即为空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->head == obj->tail;
}
判断是否为满
直接利用神奇公式
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->tail+1)%(obj->k+1)==obj->head;
}
插入和删除
插入删除,和我们分析的一样,用tail插入,用head删除,要注意的是
1.要先判断是否为空或是否为满,避免出现错误
2.在tail++或者head++的最后要%k+1,避免tail或者head超出空间下标
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(myCircularQueueIsFull(obj))
return false;
else
obj->a[obj->tail] = value;
obj->tail++;
obj->tail%=(obj->k+1);
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return false;
else
obj->head++;
obj->head%=(obj->k+1);
return true;
}
返回头节点
这个直接返回head下标对应的数据即可
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
return obj->a[obj->head];
}
返回尾节点
返回尾节点,要注意,由于之前插入时tail在插入完成后+1,所以我们要返回tail-1位置的数据,而tail有可能在开头,那么我们就需要返回数组末的数据,直接三目操作符实现
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
return obj->tail==0?obj->a[obj->k]:obj->a[obj->tail-1];
}
销毁环形链表
由于是malloc开的数组空间,直接free即可
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
总结
以上就是队列的基本用法和环形链表的实现,相对来说并不难,逻辑和代码的实现都比较清晰简单,希望大家能够掌握队列的知识。