队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的结构特点最核心的就是先进先出四个字,和栈类似,我们在实现队列这个数据结构时首先需要考虑的就是使用顺序表来实现还是使用链表来实现,而考虑的主要因素就是该结构是否可以满足或者说是否可以更方便地实现队列的核心特点先进先出。如果没有看过顺序表和链表的实现的,可以先看一下我之前的文章。
这里我们简单地分析一下,如果使用顺序表,那么实现队列就会变得比较麻烦,因为顺序表的尾插尾删效率确实很高,但它的头插头删效率就很一般了,因为顺序表的头插头删都需要我们对顺序表里面的元素进行前移或后移,这是一个时间复杂度为O(N) 的操作,效率是非常一般的,而我们如果使用顺序表来实现队列的话,我们的入队列即是尾插还是很不错,但是出队列即是头删的效率就非常一般了,所以我们在实现队列的时候不推荐使用顺序表。
而如果使用链表,链表的优势就体现出来了,因为链表对于头插头删或者尾插尾删来说效率是非常高的,只需要申请新结点后进行链接操作即可,所以我们推荐使用链表来实现队列。我们在队列的结构中需要用两个变量来分别记录队头(头结点)和队尾(尾结点)的位置以及一个变量来记录当前队列的大小,当然,如果使用双向循环链表的话甚至不用单独记录队头和队尾,不过相应地如果使用双向链表的话进行链接操作时会变得更复杂。而队列的先进先出也就是链表的尾插头删,效率非常高。
// 链式结构:表示队列
typedef int QDataType;
typedef struct QListNode
{
struct QListNode* _next;
QDataType _data;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* _front;
QNode* _rear;
int size;
}Queue;
在本篇文章中,我们就使用单链表来实现一个队列。在C语言中,我们先用一个结构体定义一个链表的结点,在这里即是这个struct QListNode的结构体并将其typedef重命名为QNode,之后我们再用一个结构体来定义一个队列,在这里即是这个struct Queue的结构体并将其typedef重命名为Queue,便于后面书写的简便性。在这个队列的结构中,我们需要定义两个QNode类型的指针来分别记录队列的队头位置和队尾位置,然后再定义一个整形类型的变量来记录当前队列的大小。同时为了方便我们以后更改队列的存储数据类型,我们可以在这里将类型进行typedef,即typedef int QDataType,将int重命名为QDataType,这样以后我们只用在这里更改类型即可更改队列的存储的数据类型。
// 初始化队列
void QueueInit(Queue* q)
{
assert(q);
q->_front = NULL;
q->_rear = NULL;
q->size = 0;
}
初始化队列的函数,我们传入一个Queue类型的指针,然后将其内部完成初始化操作。即将队头和队尾位置赋值为空,然后将队列大小赋值为0。
// 销毁队列
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->_front;
while (cur)
{
QNode* del = cur;
cur = cur->_next;
free(del);
del = NULL;
}
q->_front = NULL;
q->_rear = NULL;
q->size = 0;
}
销毁队列的函数,类似地,我们传入一个Queue类型的指针,然后遍历链表,依次使用free函数释放每个结点,这里和单向链表的销毁一模一样,最后我们再将队列的队头和队尾位置赋值为空并将队列大小赋值为0完成队列的销毁操作。
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
int QueueEmpty(Queue* q)
{
assert(q);
return q->_front == NULL;
}
判断队列是否为空的函数,这个函数非常简单,一句return q->_front == NULL就可以搞定,如果当前队列的队头位置为空,那么该队列就为空队列,这句语句就为真,为真就返回非0结果,反之不为空这句语句即为假,为假就返回0。当然这里判空的语句不一定要用队头位置是否为空来判断,用队尾位置是否为空或者队列的大小是否为0来判断也都是没问题的。
// 获取队列中有效元素个数
int QueueSize(Queue* q)
{
assert(q);
return q->size;
}
获取队列中有效元素个数的函数,这个函数非常简单,我们直接返回记录队列大小的size的值即可。
// 队尾入队列
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* pnew = (QNode*)malloc(sizeof(QNode));
if (pnew == NULL)
{
perror("malloc");
exit(-1);
}
pnew->_data = data;
pnew->_next = NULL;
if (q->size == 0)
{
q->_front = pnew;
q->_rear = pnew;
}
else
{
q->_rear->_next = pnew;
q->_rear = pnew;
}
q->size++;
}
进行入队列操作的函数,这个函数就和链表的尾插函数完全类似。首先我们需要进行申请新结点的操作,我们利用malloc函数在堆上申请一个新结点,然后将入队列的值data赋值给新结点中的_data并将_next指针初始化为空完成申请新结点的操作。之后我们就进行链接操作,这里就和单链表的尾插完全类似。我们首先进行分类讨论看当前链表是否为空,如果当前链表为空的话我们就直接让队头和队尾位置指向新开辟的结点pnew;如果不为空的话,我们就让我们的队尾结点_rear的_next指针指向新结点并将队尾结点_rear更新为新结点。最后我们将队列的大小size加1即可完成入队列的操作。
// 队头出队列
void QueuePop(Queue* q)
{
assert(q);
if (QueueEmpty(q))
{
printf("当前队列为空\n");
return;
}
if (q->_front == q->_rear)
{
free(q->_front);
q->_front = NULL;
q->_rear = NULL;
}
else
{
QNode* del = q->_front;
q->_front = del->_next;
free(del);
del = NULL;
}
q->size--;
}
进行出队列操作的函数,同理,这个函数和链表的头删函数类似,我们先进行判空操作,保证出队列时队列不能为空,如果不为空我们才进行出队列操作。然后出队列操作我们也进行一个分类讨论,讨论一下当前队列的大小是否为1,即队头q->_front是否等于队尾q->_rear,因为这涉及到我们是否需要更改队尾的位置。如果大小为1,我们就用free函数释放这一个结点,然后将队头和队尾都赋值为空;如果大小不为1,我们就先用一个变量del记录当前的队头位置,然后将队头位置_front指向队头的_next位置并释放del记录的之前的队头位置。最后我们将队列的大小size减1即可完成出队列操作。
// 获取队列头部元素
QDataType QueueFront(Queue* q)
{
assert(q);
if (q->size == 0)
{
printf("无元素\n");
return 0;
}
return q->_front->_data;
}
获取队列头部元素的函数,我们先进行一个判空操作,如果队列为空则提前返回,如果不为空就返回队头位置_front的数据。
// 获取队列队尾元素
QDataType QueueBack(Queue* q)
{
assert(q);
if (q->size == 0)
{
printf("无元素\n");
return 0;
}
return q->_rear->_data;
}
获取队列队尾元素的函数,同理,我们也先进行一个判空操作,如果队列为空则提前返回,如果不为空就返回队尾位置_rear的数据。
进行测试的main函数,用来测试我们写的队列的逻辑是否存在问题。
int main()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
printf("%d\n", QueueBack(&q));
printf("%d ", QueueFront(&q));
QueuePop(&q);
printf("%d ", QueueFront(&q));
QueuePop(&q);
printf("%d ", QueueFront(&q));
QueuePop(&q);
printf("%d ", QueueFront(&q));
QueuePop(&q);
printf("\n");
QueuePop(&q);
printf("\n");
printf("%d ", QueueSize(&q));
printf("\n");
printf("%d\n", QueueFront(&q));
printf("%d\n", QueueBack(&q));
printf("%d\n", QueueEmpty(&q));
QueueDestroy(&q);
return 0;
}
本章到这里数据结构队列的实现就已经全部完成了,队列的实现难度其实不是特别大,特别是如果亲自实现过前面的顺序表和链表的话,难度应该会更小。虽然队列看起来简单,但我们不能忽视它的重要性,它的作用包括但不限于任务调度,事件处理,缓存机制,并发控制,算法实现等方面。所以虽然结构基础,但它的意义却是非常之大,值得我们亲自去实现它。
如需源码,可在我的gitee上找到,下面是链接。
如对您有所帮助,可以来个三连,感谢大家的支持。
张靓颖–念念不忘
A-Lin–天若有情
梁静茹–慢冷
学技术学累了时可以听歌放松一下。