在计算机科学中,数据结构(data structure)是一种数据组织、管理和存储的格式。它是相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术相关。 数据结构研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系。它包含三个方面的内容:即数据的逻辑结构、数据的存储结构和数据的操作,只有这三个方面的内容完全相同,才能成为完全相同的数据结构。
数据结构是计算机四大件之一,是与计算机组成原理、操作系统、计算机网络齐名的存在,因此数据结构的重要性不言而喻。
本章将带领大家去了解队列这一重要的数据结构。
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。
与栈一样,队列的底层结构同样可以选择用数组或者链表来实现,经过对其效率进行分析之后可知底层结构使用链表会更加好一些,因为在进行出队列操作时是在对第一个数据进行操作,而数组在删除第一个元素的时间复杂度要高于链表,因此,我们选择链表作为队列的底层结构。当然,具体是使用数组还是链表还是需要根据实际情况进行选择。
typedef struct QueueNode
{
int val;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
struct QueueNode* phead;
struct QueueNode* ptail;
int size;
}Queue;
因为使用链表作为队列的底层结构,所以我们需要先定义一个链表来储存我们的每一个元素。由于队列只能从头部和尾部去进行各种操作,因此我们需要两个指针进行管理,也就是phead和ptail,一个指向队头元素,一个指向队尾元素,size则用来记录队列中的元素个数。
想要实现一个完整的队列,我们需要有以下函数接口:
//初始化队列
void QueueInit(Queue* pq);
// ⼊队列,队尾
void QueuePush(Queue* pq, int x);
// 出队列,队头
void QueuePop(Queue* pq);
//取队头数据
int QueueFront(Queue* pq);
//取队尾数据
int QueueBack(Queue* pq);
//队列判空
bool QueueEmpty(Queue* pq);
//队列有效元素个数
int QueueSize(Queue* pq);
//销毁队列
void QueueDestroy(Queue* pq);
void QueueInit(Queue* pq)
{
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
队列的初始化较为简单,只需先将头尾指针置空,元素个数size置0即可。
void QueuePush(Queue* pq, int x)
{
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
exit(-1);
printf("malloc fail!\n");
}
newnode->val = x;
newnode->next = NULL;
if (pq->phead = NULL)
{
pq->phead = newnode;
pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
由于队列中存储元素的结构是链表,因此当我们每次入队列时需要先用malloc动态开辟一块空间用来存储元素。 队列是否为空也会影响入队列,因此我们分情况讨论,当队列不为空时,我们不仅要让ptail指向新入队的元素,同时也要让前一个元素指向新入队的元素;当队列为空时,我们只需让头尾指针都指向该元素,最后将size++即可。
void QueuePop(Queue* pq)
{
if (pq->size == 0)
{
printf("empty queue!");
return;
}
QNode* temp = pq->phead;
pq->phead = pq->phead->next;
free(temp);
pq->size--;
}
进行出队列操作时,我们首先需要判断队列是否为空。当队列不为空时,我们首先要用一个临时变量来保存头指针phead此时指向的空间,这是因为当我们将头指针指向下一个元素空间后,该元素空间的地址将会找不到,从而不能释放掉这块空间。最后将size--即可。
int QueueFront(Queue* pq)
{
if (pq->size == 0)
{
printf("empty queue!");
return;
}
return pq->phead->val;
}
取队头元素都是非常简单的操作,唯一需要注意的是要先判断队列是否为空。代码如上。
int QueueBack(Queue* pq)
{
if (pq->size == 0)
{
printf("empty queue!");
return;
}
return pq->ptail->val;
}
同上。
bool QueueEmpty(Queue* pq)
{
if (pq->size == 0)
{
return true;
}
else
{
return false;
}
}
只需要判断size的大小即可,代码如上。
int QueueSize(Queue* pq)
{
return pq->size;
}
我们在结构体中定义size这个变量便是为了方便统计队列中的元素个数。
void QueueDestroy(Queue* pq)
{
if (pq->size == 0)
{
printf("empty queue!");
return;
}
QNode* pcur = pq->phead;
while (pcur != NULL)
{
QNode* next = pcur->next;
free(pcur);
pcur = next;
}
pq->phead = NULL;
pq->ptail = NULL;
}
由于队列的底层结构是链表,所以队列的销毁与链表的销毁一致,只需在最后将头尾指针置空即可。
虽然我们在上面讨论了用链表的效率要高于数组,但是当我们不考虑空间浪费时,出队列操作时直接将头指针往后减一也可完成操作,此时数组的效率和链表基本一致。但是空间的浪费我们该如何解决呢?这时我们就需要用到循环队列了,循环队列是把队列空间想象成一个环形空间,环形空间中的存储单元循环使用。除了一些简单应用之外,真正实用的队列是循环队列。关于循环队列的具体结构以及实现我们下次再进行深入探讨。
在许多算法题中,队列与栈一样可以很好的解决特定的问题,它们的奥妙当我们在做题中能够一一体会到。
关于队列这一数据结构,我们已经了解完了。虽然它的代码实现相对来说比较简单,但是想要用好队列,用对队列还是一件难事,需要我们通过大量的练习去更加深刻的了解它。
若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!