队列的定义
队列(Queue)是一种特殊的线性数据结构,它遵循先进先出(FIFO,First In First Out)的原则。这意味着最早被添加到队列中的元素将是最先被移除的元素。队列的主要操作包括入队(enqueue,在队列的尾部添加元素)和出队(dequeue,从队列的头部移除元素)。
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表。
入队列:队列的插入操作叫做入队列,进行插入操作的一端称为队尾。
出队列:队列的删除操作叫做出队列,进行删除操作的一端称为队头。
队列的实现方式
队列的实现方式有多种,包括基于数组(或循环数组)的实现、基于链表的实现等。
基于数组(或循环数组)的队列实现
这种实现方式使用一个固定大小或循环使用的数组来存储队列中的元素。数组的一个端点被视作队列的头部(front),用于执行出队操作;另一个端点被视作队列的尾部(rear),用于执行入队操作。当队列满时,需要判断是否有空间可以循环利用(即队头元素是否已被移除)。
基于链表的队列实现
基于链表的实现使用链表数据结构来存储队列中的元素。链表的头部被视作队列的头部(front),用于执行出队操作;链表的尾部被视作队列的尾部(rear),用于执行入队操作。这种实现方式更加灵活,因为链表不需要预先分配固定大小的空间,而且可以动态地扩展和收缩。
无论是基于数组还是基于链表的实现,队列都保持了其先进先出的特性,使得它在处理需要按顺序处理的元素序列时非常有用。
初始化队列
首先我们需要创建一个结点类型,类型包含了该结点的数据和指向下一结点的指针。
typedef int QDataType;//队列中存储的元素类型(这里用整型举例)
typedef struct QListNode
{
struct QListNode* next;//指针域
QDataType data;//数据域
}QListNode;
队列与普通链表又有所不同,普通链表只需要知道链表的头指针,而队列的信息包括了队头和队尾,所以我们需要再创建一个结构体用于存放队列的队头和队尾。
typedef struct Queue
{
QListNode* head;//队头
QListNode* tail;//队尾
}Queue;
队列的基本操作
我们可以先看下面这一系列操作
typedef char QDatatype;
typedef struct QueueNode
{
struct QueueNode* next;
QDatatype data;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDatatype x);
void QueuePop(Queue* pq);
int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);
QDatatype QueueFront(Queue* pq);
QDatatype QueueBack(Queue* pq);
初始化队列
// 初始化队列函数
void QueueInit(Queue* pq)
{
// 使用断言确保传入的队列指针不为空
assert(pq);
// 将队列的头部和尾部指针都初始化为NULL
// 初始化时队列为空,没有节点
pq->head = pq->tail = NULL;
// 将队列的大小初始化为0
// 队列中尚未包含任何元素
pq->size = 0;
}
在这个函数中,我们首先使用assert
宏来检查传入的队列指针pq
是否为空。如果pq
为空,assert
将触发一个运行时错误,这通常用于调试以确保程序在运行时不会因为无效的指针而崩溃。
然后,我们将队列的头部和尾部指针都初始化为NULL
,因为队列在初始化时是空的,没有任何节点。
最后,我们将队列的大小size
初始化为0,表示队列中还没有包含任何元素。
销毁队列
// 销毁队列函数
void QueueDestroy(Queue* pq)
{
// 使用断言确保传入的队列指针不为空
assert(pq);
// 初始化当前节点为队列的头部节点
QNode* cur = pq->head;
// 循环遍历队列中的每一个节点,直到没有节点为止
while (cur)
{
// 保存当前节点的下一个节点的指针
QNode* next = cur->next;
// 释放当前节点所占用的内存
free(cur);
// 将当前节点移动到下一个节点,继续释放内存
cur = next;
}
// 将队列的头部和尾部指针都设置为NULL
// 表示队列已经被销毁,不再包含任何节点
pq->head = pq->tail = NULL;
// 将队列的大小设置为0
// 表示队列中没有元素
pq->size = 0;
}
这个函数负责销毁一个队列,释放队列中所有节点所占用的内存,并将队列的头部和尾部指针以及大小重置为初始状态。
入队
// 入队操作函数
void QueuePush(Queue* pq, QDatatype x)
{
// 为新节点分配内存
QNode* newnode = (QNode*)malloc(sizeof(QNode));
// 检查内存分配是否成功
if (newnode == NULL)
{
// 如果内存分配失败,打印错误信息并返回
perror("malloc fail");
return;
}
// 将数据x赋值给新节点的data成员
newnode->data = x;
// 初始化新节点的next指针为NULL
newnode->next = NULL;
// 如果队列为空(即头部和尾部指针都为NULL)
if (pq->head == NULL)
{
// 断言确保尾部指针也为NULL(这通常是正确的,但如果不是则会导致逻辑错误)
assert(pq->tail == NULL);
// 将新节点设置为队列的头部和尾部节点
pq->head = pq->tail = newnode;
}
else
{
// 如果队列不为空,将新节点添加到队列的尾部
// 将当前尾部节点的next指针指向新节点
pq->tail->next = newnode;
// 更新尾部指针,使其指向新节点
pq->tail = newnode;
}
// 队列中的元素数量加1
pq->size++;
}
这个函数用于将一个元素x
添加到队列的尾部。
出队
// 出队操作函数
void QueuePop(Queue* pq)
{
// 断言确保队列指针不为空
assert(pq);
// 断言确保队列不为空
assert(pq->head != NULL);
// 如果队列中只有一个节点
if (pq->head->next == NULL)
{
// 释放头节点占用的内存
free(pq->head);
// 将头部和尾部指针都设置为NULL,表示队列为空
pq->head = pq->tail = NULL;
}
else
{
// 否则,队列中有多个节点
// 保存头节点的下一个节点的指针
QNode* next = pq->head->next;
// 释放头节点占用的内存
free(pq->head);
// 将头部指针指向原来的第二个节点
pq->head = next;
}
// 队列中的元素数量减1
pq->size--;
}
获取队列中元素数量
// 获取队列大小函数
int QueueSize(Queue* pq)
{
// 断言确保队列指针不为空
assert(pq);
// 返回队列中的元素数量
return pq->size;
}
这个函数用于获取队列中的元素数量。
检查队列是否为空
// 检查队列是否为空函数
bool QueueEmpty(Queue* pq)
{
// 断言确保队列指针不为空
assert(pq);
// 如果队列的大小为0,则返回true,表示队列为空
// 否则返回false,表示队列不为空
return pq->size == 0;
}
这个函数用于检查队列是否为空。它接受一个指向队列的指针pq
作为参数,并使用断言来确保这个指针是有效的。然后,它比较队列的大小(size
)是否等于0。如果等于0,说明队列中没有元素,函数返回true
;否则,队列中有元素,函数返回false
。
获取队列头部元素
// 获取队列头部元素函数
QDatatype QueueFront(Queue* pq)
{
// 断言确保队列指针不为空
assert(pq);
// 断言确保队列不为空
assert(!QueueEmpty(pq));
// 返回队列头部节点的数据
return pq->head->data;
}
获取队列尾部元素
// 获取队列尾部元素函数
QDatatype QueueBack(Queue* pq)
{
// 断言确保队列指针不为空
assert(pq);
// 断言确保队列不为空
assert(!QueueEmpty(pq));
// 返回队列尾部节点的数据
return pq->tail->data;
}
这个函数用于获取队列尾部元素的值。它接受一个指向队列的指针pq
作为参数,并使用断言来确保队列指针有效且队列不为空。
队列的尾部元素是队列中最后一个被加入的元素。由于队列的尾部节点可以通过tail
指针直接访问,因此这个函数直接返回队列尾部节点的data
成员的值。