目录
[1. 函数目的](#1. 函数目的)
[2. 参数说明](#2. 参数说明)
[3. 断言检查](#3. 断言检查)
[4. 初始化队列的头尾指针](#4. 初始化队列的头尾指针)
[5. 初始化队列的大小](#5. 初始化队列的大小)
[6. 总结](#6. 总结)
[1. 函数目的](#1. 函数目的)
[2. 参数说明](#2. 参数说明)
[3. 断言检查](#3. 断言检查)
[4. 申请新节点](#4. 申请新节点)
[5. 初始化新节点](#5. 初始化新节点)
[6. 将新节点插入队列](#6. 将新节点插入队列)
[7. 更新队列大小](#7. 更新队列大小)
[8. 总结](#8. 总结)
[1. 函数目的](#1. 函数目的)
[2. 参数说明](#2. 参数说明)
[3. 断言检查](#3. 断言检查)
[4. 判断队列是否为空](#4. 判断队列是否为空)
[5. 总结](#5. 总结)
[1. 参数检查](#1. 参数检查)
[2. 处理队列中只有一个节点的情况](#2. 处理队列中只有一个节点的情况)
[3. 处理队列中有多个节点的情况](#3. 处理队列中有多个节点的情况)
[4. 更新队列大小](#4. 更新队列大小)
[5. 总结](#5. 总结)
[1. 断言检查](#1. 断言检查)
[2. 返回队头数据](#2. 返回队头数据)
[3. 总结](#3. 总结)
[1. 断言检查](#1. 断言检查)
[2. 返回队尾数据](#2. 返回队尾数据)
[1. 注释掉的部分 (迭代计数)](#1. 注释掉的部分 (迭代计数))
[2. 直接返回 pq->size 的部分](#2. 直接返回 pq->size 的部分)
[3. 两种实现方式的比较](#3. 两种实现方式的比较)
[4. 总结](#4. 总结)
[1. 函数声明](#1. 函数声明)
[2. 参数检查](#2. 参数检查)
[3. 遍历队列并释放节点](#3. 遍历队列并释放节点)
[4. 重置队列状态](#4. 重置队列状态)
[5. 总结](#5. 总结)
一、概念与结构
**概念:**只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出大的结构特征FIFO(First In Frist Out)
**入队列:**进⾏插⼊操作的⼀端称为队尾
**出队列:**进⾏删除操作的⼀端称为队头

队列底层结构选型:
队列也可以数组和链表的结构实现,使⽤链表的结构实现更优⼀些,因为如果使⽤数组的结构,出队列在数组头上出数据,效率会⽐较低。
二、队列的实现
1、队列的初始化

cpp
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、入队列队尾

cpp
// ⼊队列,队尾
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、队列判空
cpp
//队列判空
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、出队列队头

cpp
// 出队列,队头
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、取队头数据
cpp
//取队头数据
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、取队尾数据
cpp
//取队尾数据
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、队列有效元素个数
cpp
//队列有效元素个数
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、销毁队列
cpp
//销毁队列
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
cpp
#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
cpp
#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
cpp
#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;
}