目录
一、概念与结构
概念:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出大的结构特征FIFO(First In Frist Out)
入队列:进⾏插⼊操作的⼀端称为队尾
出队列:进⾏删除操作的⼀端称为队头
队列底层结构选型:
队列也可以数组和链表的结构实现,使⽤链表的结构实现更优⼀些,因为如果使⽤数组的结构,出队列在数组头上出数据,效率会⽐较低。
二、队列的实现
1、队列的初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
1. 函数目的
-
QueueInit
函数的目的是初始化一个队列,将其设置为空队列状态。
2. 参数说明
-
Queue* pq
:这是一个指向Queue
结构体的指针,表示要初始化的队列。
3. 断言检查
-
assert(pq);
:使用assert
宏来确保传入的指针pq
不是NULL
。如果pq
是NULL
,程序会在这里终止并报错。这是一种防御性编程的手段,确保传入的指针是有效的。
4. 初始化队列的头尾指针
-
pq->phead = pq->ptail = NULL;
:将队列的头指针phead
和尾指针ptail
都设置为NULL
,表示队列中没有任何元素。
5. 初始化队列的大小
-
pq->size = 0;
:将队列的大小size
设置为0
,表示队列当前为空。
6. 总结
-
通过这个函数,队列被初始化为一个空队列,头尾指针都指向
NULL
,且队列的大小为0
。这个函数通常在创建队列后立即调用,以确保队列处于一个有效的初始状态。
2、入队列队尾
// ⼊队列,队尾
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
//申请新节点
QueueNode* newnode =(QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
//ptail newnode
if (pq->phead == NULL)
{//队列为空
pq->phead = pq->ptail = newnode;
}
else
{
//队列不为空
pq->ptail->next = newnode;
pq->ptail = pq->ptail->next;//newnode
}
pq->size++;
}
1. 函数目的
-
QueuePush
函数的目的是将一个元素x
添加到队列的尾部(队尾),并更新队列的状态。
2. 参数说明
-
Queue* pq
:指向队列的指针,表示要操作的队列。 -
QDataType x
:要入队的元素,类型为QDataType
(可能是int
、float
或其他自定义类型)。
3. 断言检查
-
assert(pq);
:使用assert
宏检查传入的队列指针pq
是否为NULL
。如果pq
是NULL
,程序会终止并报错。这是一种防御性编程的手段,确保传入的指针是有效的。
4. 申请新节点
-
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
:动态分配内存,创建一个新的队列节点newnode
。 -
if (newnode == NULL)
:检查内存分配是否成功。如果malloc
失败(返回NULL
),则输出错误信息"malloc fail!"
并调用exit(1)
终止程序。
5. 初始化新节点
-
newnode->data = x;
:将新节点的数据域data
设置为传入的值x
。 -
newnode->next = NULL;
:将新节点的next
指针设置为NULL
,表示它是队列的最后一个节点。
6. 将新节点插入队列
-
情况 1:队列为空
-
if (pq->phead == NULL)
:如果队列的头指针phead
为NULL
,说明队列当前为空。 -
pq->phead = pq->ptail = newnode;
:将队列的头指针phead
和尾指针ptail
都指向新节点newnode
,因为新节点是队列中唯一的节点。
-
-
情况 2:队列不为空
-
pq->ptail->next = newnode;
:将当前尾节点ptail
的next
指针指向新节点newnode
,将新节点链接到队列的尾部。 -
pq->ptail = pq->ptail->next;
:更新尾指针ptail
,使其指向新节点newnode
,因为新节点现在是队列的最后一个节点。
-
7. 更新队列大小
-
pq->size++;
:将队列的大小size
加 1,表示队列中新增了一个元素。
8. 总结
-
这个函数的核心逻辑是将新节点插入到队列的尾部,并更新队列的头尾指针和大小。
-
如果队列为空,新节点既是头节点也是尾节点。
-
如果队列不为空,新节点被链接到当前尾节点的后面,并成为新的尾节点。
3、队列判空
//队列判空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead == NULL && pq->ptail == NULL;
}
1. 函数目的
-
QueueEmpty
函数的目的是判断队列是否为空。如果队列为空,返回true
;否则返回false
。
2. 参数说明
-
Queue* pq
:指向队列的指针,表示要检查的队列。
3. 断言检查
-
assert(pq);
:使用assert
宏检查传入的队列指针pq
是否为NULL
。如果pq
是NULL
,程序会终止并报错。这是一种防御性编程的手段,确保传入的指针是有效的。
4. 判断队列是否为空
-
return pq->phead == NULL && pq->ptail == NULL;
:-
通过检查队列的头指针
phead
和尾指针ptail
是否都为NULL
来判断队列是否为空。 -
如果
phead
和ptail
都为NULL
,说明队列中没有元素,返回true
。 -
否则,返回
false
。
-
5. 总结
-
这个函数的逻辑非常简单,直接通过检查队列的头尾指针是否都为
NULL
来判断队列是否为空。 -
这是一种高效且直观的判空方法,时间复杂度为 O(1)。
4、出队列队头
// 出队列,队头
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
//只有一个结点的情况,避免ptail变成野指针
if (pq->ptail == pq->phead)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
//删除队头元素、
QueueNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
--pq->size;
}
1. 参数检查
-
assert(pq);
:确保传入的队列指针pq
不是空指针。如果pq
为空,程序会终止并报错。 -
assert(!QueueEmpty(pq));
:确保队列不为空。如果队列为空,无法执行出队操作,程序会终止并报错。
2. 处理队列中只有一个节点的情况
-
if (pq->ptail == pq->phead)
:检查队列中是否只有一个节点。如果队头 (phead
) 和队尾 (ptail
) 指向同一个节点,说明队列中只有一个元素。 -
free(pq->phead);
:释放这个唯一的节点。 -
pq->phead = pq->ptail = NULL;
:将队头和队尾指针都置为NULL
,表示队列为空。
3. 处理队列中有多个节点的情况
-
else
:如果队列中有多个节点,执行以下操作:-
QueueNode* next = pq->phead->next;
:保存队头节点的下一个节点(即新的队头)。 -
free(pq->phead);
:释放当前的队头节点。 -
pq->phead = next;
:将队头指针指向新的队头节点。
-
4. 更新队列大小
-
--pq->size;
:减少队列的大小(size
),表示队列中元素的数量减少了一个。
5. 总结
这段代码的核心逻辑是删除队列的队头元素,并处理队列中只有一个节点和多个节点的不同情况。通过释放队头节点的内存,并更新队头指针和队列大小,确保队列的状态正确。
5、取队头数据
//取队头数据
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->phead->data;
}
1. 断言检查
-
assert(pq);
和assert(!QueueEmpty(pq));
首先进行断言检查,确保队列指针pq
有效(非空)且队列不为空。 这有助于在开发阶段发现潜在的错误,提高代码的健壮性。
2. 返回队头数据
-
return pq->phead->data;
如果断言检查通过,则直接返回队列头结点 (pq->phead
) 的数据成员 (data
)。 因为phead
指针指向队列的第一个元素,所以pq->phead->data
就代表了队列头部的元素值。
3. 总结
-
这个函数的功能非常简单直接:获取队列头部的元素值。 它依赖于队列的内部实现,假设队列已经正确构建,并且
phead
指针指向队列的第一个元素。 断言的使用增强了函数的可靠性,防止了对空指针或空队列的访问。
6、取队尾数据
//取队尾数据
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->ptail->data;
}
1. 断言检查
-
assert(pq);
和assert(!QueueEmpty(pq));
首先进行断言检查,确保队列指针pq
有效(非空)且队列不为空。 这防止了对空指针或空队列的访问,提高了代码的健壮性。
2. 返回队尾数据
-
return pq->ptail->data;
如果断言检查通过,则直接返回队列尾部节点 (pq->ptail
) 的数据成员 (data
)。 因为ptail
指针始终指向队列的最后一个元素,所以pq->ptail->data
直接获取了队列尾部的元素值。
3. 总结
-
函数
QueueBack
的功能是获取队列的最后一个元素的值。 它简洁明了,直接通过ptail
指针访问队尾元素的数据。 断言的加入,增强了代码的可靠性,避免了潜在的错误。 该函数依赖于队列的内部实现,假设队列已经正确构建,并且ptail
指针正确地指向队列的尾部节点。
7、队列有效元素个数
//队列有效元素个数
int QueueSize(Queue* pq)
{
assert(pq);
/*int size = 0;
QueueNode* pcur = pq->phead;
while (pcur)
{
size++ ;
pcur = pcur->next;
}
return size;*/
return pq->size;
}
1. 注释掉的部分 (迭代计数)
-
断言检查:
assert(pq);
首先检查队列指针pq
是否有效(非空)。 -
初始化:
int size = 0;
初始化计数器size
为 0,用于统计队列元素个数。 -
遍历队列:
QueueNode* pcur = pq->phead;
创建一个指针pcur
,指向队列的头结点 (pq->phead
)。while (pcur)
循环遍历队列中的所有节点,直到pcur
为空(到达队列尾部)。 -
计数: 在循环体中,
size++;
每遍历一个节点,计数器size
加 1。 -
返回计数:
return size;
循环结束后,size
中保存的就是队列中元素的个数,函数返回这个值。
2. 直接返回 pq->size
的部分
这段代码直接返回队列结构体中的 size
成员变量。 这假设队列结构体中已经维护了一个 size
变量,用于实时跟踪队列中元素的个数。 每次进行入队或出队操作时,都会更新这个 size
变量。
3. 两种实现方式的比较
-
迭代计数: 这种方式每次调用
QueueSize
函数都需要遍历整个队列,时间复杂度为 O(n),其中 n 为队列中元素个数。 它不需要维护额外的size
变量,代码相对简单。 -
直接返回
pq->size
: 这种方式的时间复杂度为 O(1),效率更高。 但是,需要在队列的入队和出队操作中额外维护size
变量,增加了代码的复杂度。
4. 总结
注释掉的部分是通过遍历队列来计算大小,时间复杂度较高;而 return pq->size;
是通过维护一个计数器来直接返回大小,时间复杂度为 O(1),效率更高。 哪种方式更好取决于具体的队列实现和性能要求。 如果性能是关键因素,那么维护一个 size
变量是更优的选择。 如果简洁性更重要,或者队列的实现不允许维护 size
变量,那么迭代计数的方法也是可行的。
8、销毁队列
//销毁队列
void QueueDestroy(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* pcur = pq->phead;
while (pcur)
{
QueueNode* next = pcur->next;
free(pcur);
pcur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
1. 函数声明
-
函数名为
QueueDestroy
,返回类型为void
,表示该函数不返回任何值。 -
参数
Queue* pq
是一个指向队列的指针,用于操作队列。
2. 参数检查
-
assert(pq);
:使用assert
宏检查传入的队列指针pq
是否为空。如果pq
为空,程序会终止并报错。这一步是为了防止空指针导致的程序崩溃。 -
assert(!QueueEmpty(pq));
:使用assert
宏检查队列是否为空。如果队列为空(即QueueEmpty(pq)
返回true
),程序会终止并报错。这一步是为了确保队列中有数据可以销毁。
3. 遍历队列并释放节点
-
QueueNode* pcur = pq->phead;
:定义一个指针pcur
,初始化为指向队列的头节点(pq->phead
)。 -
while (pcur)
:使用while
循环遍历队列中的每一个节点,直到pcur
为空(即遍历完所有节点)。-
QueueNode* next = pcur->next;
:在释放当前节点之前,先保存当前节点的下一个节点指针next
,以便后续继续遍历。 -
free(pcur);
:释放当前节点pcur
的内存。 -
pcur = next;
:将pcur
指向下一个节点,继续遍历。
-
4. 重置队列状态
-
pq->phead = pq->ptail = NULL;
:将队列的头指针phead
和尾指针ptail
都设置为NULL
,表示队列为空。 -
pq->size = 0;
:将队列的大小size
设置为0
,表示队列中没有元素。
5. 总结
-
该函数的核心逻辑是遍历队列中的所有节点,逐个释放节点的内存,最后将队列重置为空状态。
-
通过
assert
检查确保了队列指针的有效性和队列非空,避免了潜在的错误。
三、完整实现队列的三个文件
Queue.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义队列结构
typedef int QDataType;
typedef struct QueueNode
{
QDataType data;
struct QueueNode* next;
}QueueNode;
typedef struct Queue
{
QueueNode* phead;
QueueNode* ptail;
int size;//保存队列有效数据个数
}Queue;
void QueueInit(Queue* pq);
// ⼊队列,队尾
void QueuePush(Queue* pq, QDataType x);
// 出队列,队头
void QueuePop(Queue* pq);
//队列判空
bool QueueEmpty(Queue* pq);
//取队头数据
QDataType QueueFront(Queue* pq);
//取队尾数据
QDataType QueueBack(Queue* pq);
//队列有效元素个数
int QueueSize(Queue* pq);
//销毁队列
void QueueDestroy(Queue* pq);
Queue.c
#include"Queue.h"
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
// ⼊队列,队尾
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
//申请新节点
QueueNode* newnode =(QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
//ptail newnode
if (pq->phead == NULL)
{//队列为空
pq->phead = pq->ptail = newnode;
}
else
{
//队列不为空
pq->ptail->next = newnode;
pq->ptail = pq->ptail->next;//newnode
}
pq->size++;
}
//队列判空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead == NULL && pq->ptail == NULL;
}
// 出队列,队头
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
//只有一个结点的情况,避免ptail变成野指针
if (pq->ptail == pq->phead)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
//删除队头元素、
QueueNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
--pq->size;
}
//取队头数据
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->phead->data;
}
//取队尾数据
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->ptail->data;
}
//队列有效元素个数
int QueueSize(Queue* pq)
{
assert(pq);
/*int size = 0;
QueueNode* pcur = pq->phead;
while (pcur)
{
size++ ;
pcur = pcur->next;
}
return size;*/
return pq->size;
}
//销毁队列
void QueueDestroy(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* pcur = pq->phead;
while (pcur)
{
QueueNode* next = pcur->next;
free(pcur);
pcur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
test.c
#include"Queue.h"
void QueueTest01()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePop(&q);
//QueuePop(&q);
//QueuePop(&q);
//QueuePop(&q);
//QueuePop(&q);
//printf("head:%d\n", QueueFront(&q));
//printf("tail:%d\n", QueueBack(&q));
printf("size:%d\n", QueueSize(&q));
QueueDestroy(&q);
}
int main()
{
QueueTest01();
return 0;
}