队列——之二

[toc]

参考:

前情提要:


1. 定义

队列(Queue):具有一定操作约束的线性表,线性结构的两种常见应用之一,是一种可以实现”先进先出”的存储结构,类似于人排队买票。

  • 插入和删除操作:只能在一端(front)插入,而在另一端(rear)删除
  • 数据插入:入队列(AddQ)
  • 数据删除:出队列(DeleteQ)
  • 先进先出:FIFO

2. 分类

  1. 静态队列:用数组实现 静态队列通常都必须是循环队列
  2. 链式队列:用链表实现 (比较简单)

3. 应用

所有和时间有关的事件都有队列的影子。

4. 学习循环队列需弄清楚的 7 个问题

静态队列通常都必须是循环队列,为了减少内存浪费。
循环队列的讲解:

  1. 静态队列为什么必须是循环队列
  2. 循环队列需要几个参数来确定
  3. 循环队列各个参数的含义
  4. 循环队列入队伪算法讲解
  5. 循环队列出队伪算法讲解
  6. 如何判断循环队列是否为空
  7. 如何判断循环队列是否已满
  1. 静态队列为什么必须是循环队列:

    1

    如图,现在如果一个数组里面存了四个元素,一开始 队头 front 就指向第一个有效元素,而 队尾 real 指向最后一个元素的下一个元素。

    • 如果进行出队操作,先进先出,则从队头 front 开始先出,假如出队到数组下标为 2 的元素位置,此位置变成了队头 front,那么数组下标为 0/1 的元素内存被释放。出队后的数组元素内存无法重新入队,因为入队是从队尾 rear 开始,不能从队头 front 之前进行入队,而且数组中入队操作达到数组最大下标后,就无法入队操作。同理出队到数组最大下标,也无法出队操作,数组卡死,所以对静态队列必须使用循环队列来提高数组的利用率。

    • 如果要删除元素(出队),front 只能加(往数组最大下标方向增加);如果要增加元素(入队),rear 只能加(往数组最大下标方向增加)。(按照一般数组的方法)。rear 指向当前队列的下一个位置。 队尾 rear 类似于链表创建(尾插法)和链表遍历算法的演示PNODE create_list(void)此函数中的PNODE pTail 指向尾节点

    也就是说:

    • 增加元素时,只能在 rear 一端增加,即 rear 向上移。删除元素时,只能在 front 一端删除元素,即 front 向上移。

    • 但是如果一直增增删删,那么就会造成 rear 端溢出,而 front 端浪费,所以对于这种情况,可以采用循环队列的形式,即当 rear 已经指向数组最后一个元素时,那么就可以转而将 rear 指向数组的第一个空出来的空间。

  2. 循环队列需要几个参数来确定
    需要 2 个参数来确定:front 和 rear

  3. 循环队列各个参数的含义:
    2 个参数在不同场合有不同的含义

    1. 队列初始化
      front 和 rear 的值相等,均为 0
    2. 队列非空
      front 代表的是队列的第一个元素
      rear 代表的是队列的最后一个有效元素的下一个元素
    3. 队列空
      front 和 rear 的值相等,但不一定为 0
  4. 循环队列入队伪算法讲解:
    准备工作:

    1. 入队前要先判断 rear 的位置,因为 rear 有可能正指向数组最后一个元素,所以 rear + 1 就越界了。
    2. 如何知道 rear 是否快越界了:rear + 1 后对整个数组长度求余数(%),余数为 0,则 rear + 1 处于最后一个数组元素位置,即将越界。

    两步完成:

    1. 将值存入rear 所代表的位置
    2. 错误的写法:rear = rear + 1;
      正确的写法是:rear = (rear+1) % 数组的长度
  5. 循环队列出队伪算法讲解:
    同入队的伪算法一样:
    front =(front + 1)% 数组的长度

  6. 如何判断循环队列是否为空:
    front == rear。
    即如果 front 与 rear 的值相等,则该队列就一定为空。

  7. 如何判断循环队列是否已满:

    6

    已知:上图中 front 的值和 rear 的值没有规律,即可以大(3>1),小(0<4),等(f=r)。
    刚开始 f=r 一定为空,那么之后经过一个循环后又 r=f 了,但此时 r=f 为满,即不能通过 r=f 这个条件来判断其既是空又是满。所以必须通过 f和r 的其他关系来判断其为满。

    所以有两种方式:

    1. 多增加一个标识是否满的参数(但每次对队列进行操作这个标志参数都要更新,浪费系统资源)
    2. 少用一个元素 【通常用此种方式】
      如果 front和rear 的值相差 1,且 front>rear,则证明队列已满。

    用 C 语言伪算法表示为:

    if ((rear+1)%数组长度==front)
      已满
    else
      未满

    7

5. 代码示例

5.1 静态队列(循环队列)

关于 Init(QUEUE* pQ) 的内存分配方式:有 2 种(不过没什么区别感觉)

  1. malloc

    pQ->pBase = (int *)malloc(sizeof(int)* MaxSize);   // 长度为 MaxSize 个元素的数组
  2. static

    //静态分配,不使用动态内存分配
    static int staticArray[MaxSize];
    pQ->pBase = staticArray;

5.1.1 C——郝斌老师

/**
* @author: @Akari
* @date: 2024-01-05 17:15:07
**/
#include <stdio.h>
#include <stdlib.h>
#define MaxSize 20 // 队列最大长度
typedef struct Queue{
int* pBase;
int front; // 队头下标
int rear; // 队尾下标
}QUEUE;

void Init(QUEUE* pQ); // 初始化
bool Enter(QUEUE* pQ, int val); // 入队
bool Out(QUEUE* pQ, int* pVal); // 出队
bool Is_Full(QUEUE* pQ); // 是否已满
bool Is_Empty(QUEUE* pQ); // 是否为空
void Traverse(QUEUE* pQ); // 遍历

int main(void)
{
QUEUE Q;
Init(&Q);
Enter(&Q, 1);
Enter(&Q, 2);
Enter(&Q, 3);
Enter(&Q, 4);
Enter(&Q, 5);
Traverse(&Q);

int pVal;
if ( Out(&Q, &pVal) )
printf("出队成功!\n出队的元素是:%d\n", pVal);
Traverse(&Q);

return 0;
}

void Init(QUEUE* pQ){

/* 静态分配,不使用动态内存分配
static int staticArray[MaxSize];
pQ->pBase = staticArray;*/
pQ->pBase = (int *)malloc(sizeof(int) * MaxSize); // 长度为 MaxSize 个元素的数组
if ( NULL == pQ->pBase ){
printf("动态内存分配失败!\n");
exit(-1);
} else {
pQ->front = 0; // 队头下标为 0
pQ->rear = 0; // 队尾下标为 0
}
}

bool Enter(QUEUE* pQ, int val){
if ( Is_Full(pQ) ){
return false;
} else {
pQ->pBase[pQ->rear] = val; // 从队尾开始,数据入队
pQ->rear = (pQ->rear + 1) % MaxSize; // 队尾 rear 加 1
return true;
}
}

bool Out(QUEUE* pQ, int* pVal){
if ( Is_Empty(pQ) ){
return false;
} else {
*pVal = pQ->pBase[pQ->front]; // 从 front 开始出队
pQ->front = (pQ->front + 1) % MaxSize; // 获取新的队头
return true;
}
}

/*
队列满时 rear/front 紧挨着,由于是循环所以对数组长度取余,
且 rear 是最后一个有效元素的下一个元素,
rear 指向的元素是空起来不使用的,空一元素判断队满。
*/
bool Is_Full(QUEUE* pQ){
if ( (pQ->rear + 1) % MaxSize == pQ->front )
return true;
else
return false;
}

bool Is_Empty(QUEUE* pQ){
if ( (pQ->front == pQ->rear) )
return true;
else
return false;
}

void Traverse(QUEUE* pQ){
int i = pQ->front; // 获取队头,从队头开始出队
while ( i != pQ->rear ){ // 是否出队到队尾
printf("%d ", pQ->pBase[i]);
i = (i + 1) % MaxSize; // 通过空一个元素不使用,判断是否到队尾,front+1 后对数组长度取余
}
printf("\n");
}

5.1.2 C——浙大数据结构

(浙大版)数据结构(四)队列

  • fprintf 函数
    fprintf 是 C 语言中的一个函数,用于将格式化的数据写入到文件流中。具体来说,fprintf 的函数原型:

    int fprintf(FILE *stream, const char *format, ...);
    • stream 是一个指向文件流的指针,表示数据将被写入到哪个文件流中。在上述代码中,使用的是 stderr,它代表标准错误流。
    • format 是一个字符串,包含了格式化输出的控制字符和要输出的数据。
    • 表示可变数量的参数,即要写入的数据。

    示例:

    //使用了 fprintf 来将字符串 "内存分配失败" 写入到标准错误流中。
    //这是一种报告错误的方式,通常用于指示程序在运行时遇到了问题。
    Q = (Queue)malloc(sizeof(struct QNode));
    if ( NULL == Q ){
    fprintf(stderr, "内存分配失败\n");
    exit(EXIT_FAILURE);
    }
    • **fprintf(stderr, “内存分配失败\n”)**:使用 fprintf 函数将错误消息写入标准错误流 (stderr)stderr 是一个专门用于输出错误信息的流,与标准输出 (stdout) 区分开来。在这里,用于将错误消息输出到屏幕上。

    • exit(EXIT_FAILURE)exit 函数用于终止程序的执行。EXIT_FAILURE 是一个宏,表示程序以失败的状态退出。这是一个标准的返回代码,用于表示程序没有成功执行。

队列的顺序存储结构通常由一个一维数组和一个记录队列头元素位置的变量 front 以及一个记录队列尾元素位置的变量 rear 组成,其中 front 指向整个队列的头一个元素的再前一个,rear 指向的是整个队列的最后一个元素,从 rear 入队,从 front 出队,且仅使用 n-1 个数组空间。


/**
* @author: @Akari
* @date: 2024-01-08 11:39:47
**/
#include <stdio.h>
#include <stdlib.h>
#define MaxSize 100
typedef int ElementType; // 假定是int类型
typedef struct QNode{
ElementType Data[MaxSize];
int front; // 记录队头
int rear; // 记录队尾
}*Queue;

Queue createQueue(); // 初始化队列
void addQ(Queue Q, ElementType e); // 入队
int isFull(Queue Q); // 判断队列是否已满
ElementType deleteQ(Queue Q); // 出队
int isEmpty(Queue Q); // 判断队列是否为空

int main(void)
{
Queue Q;
Q = createQueue();

addQ(Q, 1);
printf("1入队\n");
addQ(Q, 2);
printf("2入队\n");
addQ(Q, 3);
printf("3入队\n");

printf("%d出队\n", deleteQ(Q));
printf("%d出队\n", deleteQ(Q));

return 0;
}

// 初始化
Queue createQueue(){
Queue Q;
Q = (Queue)malloc(sizeof(struct QNode));
if ( NULL == Q ){
fprintf(stderr, "内存分配失败\n");
exit(EXIT_FAILURE);
}
Q->front = -1;
Q->rear = -1;

return Q;
}

// 判断队列是否已满
int isFull(Queue Q){
return ((Q->rear + 1) % MaxSize == Q->front);
}

// 入队
void addQ(Queue Q, ElementType e){
if ( isFull(Q) ){
printf("队列已满");
return;
} else {
Q->rear = (Q->rear + 1) % MaxSize;
Q->Data[Q->rear] = e;
}
}

// 判断队列是否为空
int isEmpty(Queue Q){
return ((Q->rear == Q->front));
}

// 出队
ElementType deleteQ(Queue Q){
if ( isEmpty(Q) ){
printf("队列为空");
return 0;
} else {
Q->front = (Q->front + 1) % MaxSize;
return Q->Data[Q->front];
}
}

5.1.3 C++——王道

5.1.3.1 C++ 引用

主要区别是使用了 C++ 的引用,而不是传递指针。 引用是 C++ 中的特性,允许在函数中修改传递的参数,而无需显式地使用指针

  1. 定义:
    在C++中,引用是一个别名,它提供了对变量的另一个名称。引用在C++中是通过使用 & 符号进行声明和定义的。引用必须在初始化时被绑定到一个对象,而且一旦绑定,它将一直引用该对象。

  2. 基本语法:

    type &refName = originalVariable;

    其中,type 是原始变量的类型,refName 是引用的名称,originalVariable 是原始变量。引用的声明和初始化必须同时进行。

  3. 示例:

    #include <iostream>
    using namespace std;
    int main()
    {
    int a = 42;
    int &b = a;

    cout << "a: " << a << endl;
    cout << "b: " << b << endl;

    b = 99; // 修改引用,也会修改原始变量
    cout << "a: " << a << endl;
    cout << "b: " << b << endl;
    return 0;
    }

    其中:b 是 a 的引用,它们实际上指向同一个内存地址。因此,对 b 的修改也会影响到 a。

  4. 引用有一些重要的特性和规则:

    1. 引用必须在声明时初始化,而且一旦初始化,就无法改变引用的目标。
    2. 引用不占用额外的内存空间,它只是目标变量的另一个名字。
    3. 引用通常用于函数参数,允许在函数中直接操作原始变量,而不是其副本。
    4. 引用用于提供对对象的更直观、更自然的访问。

    引用在C++中广泛用于函数传递、返回引用值、操作符重载等场景。

  5. 引用和指针的异同:
    引用(Reference)和指针(Pointer)是 C++ 中两种用于引用和操作变量的机制,它们有相似之处,也有一些重要的区别。以下是引用和指针的异同点:

相同点:

  1. 别名机制:
    引用和指针都提供了对变量的别名机制,可以通过它们来访问和操作变量。
  2. 用于传递参数:
    引用和指针都可以用于函数参数,使得函数能够直接操作原始变量,而不是对变量的副本进行操作。
  3. 用于修改变量值:
    通过引用或指针,可以修改原始变量的值,而不仅仅是传递它们的副本。

不同点:

  1. 语法和操作:
    • 引用:
      • 引用在声明时使用 & 符号,例如 int &ref = variable;。
      • 引用一旦初始化,就不能再引用其他变量,一直引用同一个对象。
      • 引用不需要使用解引用操作符(*)来访问目标对象。
      • 引用不能为 NULL,引用必须在初始化时指向一个有效的对象。
    • 指针:
      • 指针在声明时使用 * 符号,例如 int *ptr = &variable;。
      • 指针可以在任何时候重新指向其他对象。
      • 使用解引用操作符(*)来访问目标对象。
      • 指针可以为 NULL,即指向空地址,表示不指向任何有效的对象。
  2. 内存占用:
    • 引用:
      • 引用在内存中不占用额外的空间,它只是目标变量的别名。
    • 指针:
      • 指针需要占用额外的内存空间来存储变量的地址。
  3. 空引用和空指针:
    • 引用:
      • 引用在定义时必须初始化,不存在空引用的概念。
    • 指针:
      • 指针可以为 NULL,表示不指向任何有效的对象,称为空指针。
  4. 操作符重载:
    • 引用不支持操作符重载。
    • 指针支持操作符重载,可以通过指针实现一些底层的操作。
  5. 适用场景:
    • 引用:
      • 适用于不需要改变指向的情况,提供了更直观的语法。
    • 指针:
      • 适用于需要在运行时改变指向的情况,更灵活。
  6. 访问对象成员:
    • 引用:
      • 引用使用 .点运算符访问对象的成员,因为引用本质上是一个别名,就是绑定到某个对象上的别名,它可以像对象本身一样访问成员。
    • 指针:
      • 当有一个指向结构体或类的指针时,使用->箭头运算符来访问对象的成员。

在选择使用引用还是指针时,取决于具体的场景和需求:通常情况下,引用更适用于作为函数参数传递,而指针更适用于需要动态分配内存、在运行时改变指向等场景。


/**
* @author: @Akari
* @date: 2024-01-08 12:22:10
**/
#include <stdio.h>
#include <stdlib.h>
/**定义模块**/
#define MaxSize 100
typedef int ElemType;
typedef struct{
ElemType data[MaxSize]; // 用静态数组存放队列元素
int front, rear; // 队头指针和队尾指针
}SqQueue;

// 函数声明
void initQueue(SqQueue &Q); // 初始化
bool isEmpty(SqQueue Q); // 判空
bool isFull(SqQueue Q); // 判满
bool enQueue(SqQueue &Q, ElemType e); // 入队
bool outQueue(SqQueue &Q, ElemType &e); // 出队,用 e 返回
bool getHead(SqQueue &Q, ElemType &e); // 获取队头元素,用 e 返回
/**定义模块**/

/**测试模块**/
void printQueue(SqQueue Q);
void testQueue();
/**测试模块**/

int main(void)
{
testQueue();
return 0;
}

/**实现模块**/
// 初始化队列
void initQueue(SqQueue &Q){
Q.rear = Q.front = 0; // 初始化时,队头队尾都指向 0
}

// 判断队列是否为空
bool isEmpty(SqQueue Q){
if ( Q.rear == Q.front ){
return true;
} else {
return false;
}
}

// 判断队列是否为满
bool isFull(SqQueue Q){
if ( (Q.rear + 1) % MaxSize == Q.front ){ // 牺牲一个单元区分队空和队满
return true;
} else {
return false;
}
}

// 入队
bool enQueue(SqQueue &Q, ElemType e){
// 如果队列已满直接返回
if ( isFull(Q) ){
return false;
}
Q.data[Q.rear] = e;
Q.rear = (Q.rear + 1) % MaxSize; // 通过取余操作让整个队列循环起来
return true;
}

// 出队
bool outQueue(SqQueue &Q, ElemType &e){
// 如果队列为空直接返回
if ( isEmpty(Q) ){
return false;
}
e = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize;
return true;
}

bool getHead(SqQueue &Q, ElemType &e){
if ( isEmpty(Q) ){
return false;
}
e = Q.data[Q.front];
return true;
}
/**实现模块**/

/**测试模块**/
// 打印整个队列
void printQueue(SqQueue Q){
printf("开始打印队列\n");
while ( Q.front != Q.rear ){
printf("%d ", Q.data[Q.front]);
Q.front = (Q.front + 1) % MaxSize; // 输出后更新下标
}
printf("结束打印\n");
}

// 测试
void testQueue(){
printf("开始测试\n");
SqQueue Q;
initQueue(Q);

if ( enQueue(Q, 1) ){
printf("入队成功!\n");
} else {
printf("入队失败!\n");
}
if ( enQueue(Q, 2) ){
printf("入队成功!\n");
} else {
printf("入队失败!\n");
}
if ( enQueue(Q, 3) ){
printf("入队成功!\n");
} else {
printf("入队失败!\n");
}
printQueue(Q);

int e;
if ( outQueue(Q, e) ){
printf("出队成功,弹出的元素是:%d\n", e);
} else {
printf("出队失败!");
}
if ( getHead(Q, e) ){
printf("获取队头成功!队头元素是:%d\n", e);
} else {
printf("获取队头失败!\n");
}
if ( isEmpty(Q) ){
printf("队空\n");
} else {
printf("队非空\n");
}

printf("结束测试\n");
}
/**测试模块**/

5.2 链式队列

5.2.0 头结点有无与否

在链式队列中,是否使用头结点主要影响链表的头指针和尾指针的处理方式

  1. 带头结点的链式队列:

    • 头结点存在: 链表中会额外添加一个头结点,头结点的 Next 指针指向队列的头元素,而尾指针指向队列的尾元素。这样,头结点的存在使得链表中的每个节点都有一个前驱节点。
    • 操作方便: 在插入和删除操作时,无需特殊处理头节点的情况,简化了代码逻辑。
    • 判空方便: 判断队列是否为空可以通过检查头结点的 Next 指针是否为空来实现。
  2. 不带头结点的链式队列:

    • 头结点不存在: 链表的头指针直接指向队列的头元素,而尾指针指向队列的尾元素。链表中的第一个节点即为队列的头元素。
    • 操作需特殊处理: 在插入和删除操作时,需要特殊处理头节点的情况,因为头节点和其他节点的处理方式不同。
    • 判空需特殊处理: 判断队列是否为空通常需要检查头指针和尾指针是否相等,因为没有头结点来帮助判断。

选择是否使用头结点主要取决于具体的设计需求和实现偏好。
带头结点的链表在某些情况下(适用于对插入和删除操作的简洁性有较高要求的情况)对边界情况的处理上更加方便,可以使代码更加简洁;而不带头结点的链表(适用于对内存占用比较敏感的情况,且可以接受插入和删除操作的代码逻辑稍微复杂一些)更接近实际队列的逻辑。

5.2.1 C++——郝斌老师

5.2.1.1 Clear();

错误写法:

void Clear(){
while( !this->isEmpty() )
{
int val;
this->outQueue(val);
}
}

正确写法:

void Clear(){
int val;
Out_Queue(val);
}

不需要使用循环:在每次调用 Out_Queue 之后,函数内部已经包含了循环的逻辑。只要队列不为空,就会一直执行出队操作。因此,在 Clear_Queue 中只需调用一次 Out_Queue 即可达到清空队列的目的。

5.2.1.2 ~Queue();

错误写法:

~Queue(){
this->Clear();
delete this->pHead;
}

正确写法:

~Queue(){
Clear();
delete pHead;
}

在析构函数中不需要显式调用 this->Clear(),因为析构函数会在对象销毁时自动调用。

在C++中,类的析构函数(Destructor)是一个特殊的成员函数,它在对象被销毁时自动调用。析构函数的作用是释放对象占用的资源,执行清理工作。

在示例代码中,析构函数的主要目的是清理队列,即释放队列中节点所占用的内存。已经定义了 Clear 函数来完成这个清理工作,所以在析构函数中调用 Clear 是合理的。

然而,根据C++的析构函数自动调用机制,如果析构函数中没有显式调用 Clear,对象销毁时也会自动调用 Clear,因为 Clear 包含了释放队列中节点内存的逻辑。

所以,可以不在析构函数中显式调用 Clear,而是依赖析构函数自动调用它。这样的做法更加简洁,因为析构函数的主要职责是清理资源,而 Clear 已经负责了这部分工作。


带头结点。

/**
* @author: @Akari
* @date: 2024-01-08 20:05:37
**/

// 带头结点版本。

#include <iostream>
using namespace std;

typedef struct Node{
int data;
struct Node *pNext;
}NODE, *PNODE;

class Queue{
private:
PNODE pHead, pTail; // pHead指向无用的头结点 pHead->pNext才是指向队首元素, pTail指向队尾元素
public:
// 构造函数
Queue() : pHead(new NODE), pTail(pHead){ // 在构造函数中使用初始化列表
this->pHead->pNext = nullptr;
}

// 析构函数:在对象被销毁时自动调用
~Queue(){
Clear();
delete pHead;
}

// 遍历队列元素
void Traverse() const{
if ( isEmpty() ){
cout << "队列为空,无法遍历!" << endl;
return;
}

PNODE pTemp = this->pHead->pNext;
while ( pTemp != nullptr ){
cout << pTemp->data << " ";
pTemp = pTemp->pNext;
}
cout << endl;
}

// 判空
bool isEmpty() const{
// 简化返回语句
return (this->pHead == this->pTail);
}

// 入队
void enQueue(int val){
PNODE pNew = new NODE;
pNew->data = val;
pNew->pNext = nullptr;

this->pTail->pNext = pNew; // 将pNew挂到队列尾部
this->pTail = pNew; // 注意是尾指针上移
}

// 出队
bool outQueue(int &val){
if ( isEmpty() ){
cout << "队列为空,无法出队!" << endl;
return false;
}
PNODE pTemp = this->pHead->pNext; //pHead不是要删除的队首元素,pHead->pNext所指向的元素才是要删除的元素
val = pTemp->data; // 保存出队元素
this->pHead->pNext = pTemp->pNext; // 更新首节点

delete pTemp;
pTemp = nullptr;

if (nullptr == this->pHead->pNext){ // 如果队列为空
this->pTail = this->pHead; // 尾指针要指向无用的头结点,则将 pTail 移回 pHead
}
return true;
}

// 清空队列
void Clear(){
int val;
outQueue(val);
}
};

int main(void)
{
Queue Q;

for ( int i = 0; i < 5; i++ ){
Q.enQueue(i + 1);
}
Q.Traverse();

int val;
if ( Q.outQueue(val) ){ // 用引用代替&val
cout << "出队成功,出队的元素是: " << val << endl;
} else
cout << "出队失败!" << endl;
Q.Traverse();

Q.Clear();
return 0;
}

5.2.2 C——浙大数据结构

CSDN——数据结构(四)队列

不带头结点。

​队列的链式存储结构也可以用一个单链表实现。插入和删除操作分别在链表的两头进行,front 在链表头,rear 在链表尾,从 rear 入队,从 front 出队。

/**
* @author: @Akari
* @date: 2024-01-08 21:55:20
**/

// 链式队列(不带带头节点版本)

#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;

// 链表节点
struct Node{
ElemType data;
struct Node *Next;
};

// 队列结构
typedef struct QNode{
struct Node *rear; // 指向队尾结点
struct Node *front; // 指向队头结点
}*Queue;

Queue createQueue(); // 初始化队列
void addQueue(Queue Q, ElemType e); // 入队
ElemType deleteQueue(Queue Q); // 出队
int isEmpty(Queue Q); // 判空

int main(void)
{
Queue Q;
Q = createQueue();

printf("入队 5\n");
addQueue(Q,5);
printf("入队 4\n");
addQueue(Q,4);
printf("入队 3\n");
addQueue(Q,3);

printf("出队 %d\n", deleteQueue(Q));
printf("出队 %d\n", deleteQueue(Q));
printf("出队 %d\n", deleteQueue(Q));
printf("%d\n", deleteQueue(Q));

return 0;
}

Queue createQueue(){
Queue Q;
Q = (Queue)malloc(sizeof(struct QNode));
Q->front = NULL;
Q->rear = NULL;
return Q;
}

int isEmpty(Queue Q){
return (Q->front == NULL ); // 头结点为空时队空
}

void addQueue(Queue Q, ElemType e){
struct Node *p;
p = (struct Node*)malloc(sizeof(struct Node));

p->data = e;
p->Next = NULL;
if ( NULL == Q->front ){
// 不带头节点插入第一个元素时,需做特殊处理
Q->rear = Q->front = p;
} else {
// 非第一个元素
Q->rear->Next = p; // 新节点插入到 rear 之后
Q->rear = p; // 修改尾指针
}
}

ElemType deleteQueue(Queue Q){
if ( isEmpty(Q) ){ // 即 Q.front == NULL
printf("队列空 ");
return 0;
}
struct Node *p = Q->front; // 用指针 p 记录队头
ElemType x = p->data; // 用 x 返回队头元素

// 不带头节点最后一个节点出队时,需做特殊处理
if ( Q->rear == p ){
Q->rear = Q->front = NULL; // 修改 rear 指针为空
} else {
Q->front = p->Next; // 队头指向下一个队头
}
free(p);
return x;
}

5.2.3 C++——王道

带头结点。

/**
* @author: @Akari
* @date: 2024-01-08 21:55:58
**/

// 链式队列(带头结点版本)

#include <stdio.h>
#include <stdlib.h>

/**定义模块**/
// 链表节点
typedef struct LinkNode{
int data;
struct LinkNode *Next;
}LinkNode;

// 队列结构
typedef struct{
LinkNode *front, *rear; // 头指针和尾指针
}LinkQueue;

// 函数声明
void initQueue(LinkQueue &Q); // 初始化
bool enQueue(LinkQueue &Q, int x); // 入队
bool deQueue(LinkQueue &Q, int &x); // 出队
bool getHead(LinkQueue Q, int &x); // 获取头元素
bool isEmpty(LinkQueue Q); // 判空
void clearQueue(LinkQueue &Q); // 清空
/**定义模块**/

/**实现模块**/
void initQueue(LinkQueue &Q){
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
if ( Q.front == nullptr ){
fprintf(stderr, "内存分配失败\n");
exit(EXIT_FAILURE);
}
Q.front->Next = nullptr; // 初始化时,front、rear 都指向头结点
}

bool enQueue(LinkQueue &Q, int x){
// 判满?链式存储一般不需要判满,除非内存不足
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
if ( s == nullptr ){
fprintf(stderr, "内存分配失败\n");
exit(EXIT_FAILURE);
}

s->data = x;
s->Next = nullptr;
Q.rear->Next = s; // 新节点插入到 rear 之后
Q.rear = s; // 更新尾指针
return true;
}

bool deQueue(LinkQueue &Q, int &x){
if ( isEmpty(Q) ){
return false;
}

LinkNode *p = Q.front->Next; // 指针p 记录队头元素
x = p->data; // x 返回队头元素
Q.front->Next = p->Next; // 修改头节点的Next指针

// 最后一个节点出队时
if ( Q.rear == p ){
Q.rear = Q.front; // 修改rear指针
}
free(p);
return true;
}

bool getHead(LinkQueue Q, int &x){
if ( isEmpty(Q) ){
return false;
}
x = Q.front->Next->data;
return true;
}

bool isEmpty(LinkQueue Q){
return (Q.front == Q.rear);
}

void clearQueue(LinkQueue &Q){
int x;
while ( deQueue(Q, x) ){
// 依次出队,释放所有节点内存
}
free(Q.front); // 释放头结点内存
}
/**实现模块**/

/**测试模块**/
// 打印函数
void printQueue(LinkQueue Q){
printf("开始打印\n");

LinkNode *temp = Q.front->Next; // 使用临时指针 temp,避免改变原视队列的状态

while ( temp != Q.rear ){
printf("%d ", temp->data);
temp = temp->Next;
}
printf("\n打印完成\n");
}

// 测试函数
void testLinkQueue(){
printf("开始测试!\n");
LinkQueue Q;
initQueue(Q);

if ( enQueue(Q, 1) ){
printf("入队成功 1\n");
} else {
printf("入队失败\n");
}
if ( enQueue(Q, 2) ){
printf("入队成功 2\n");
} else {
printf("入队失败\n");
}
if ( enQueue(Q, 3) ){
printf("入队成功 3\n");
} else {
printf("入队失败\n");
}
printQueue(Q);

int x;
if ( deQueue(Q, x) ){
printf("出队成功,弹出的元素是 %d\n", x);
} else {
printf("出队失败\n");
}
if ( getHead(Q, x) ){
printf("获取队头成功,队头元素是 %d\n", x);
} else {
printf("获取队头元素失败\n");
}
if ( isEmpty(Q) ){
printf("队空\n");
} else {
printf("队非空\n");
}

clearQueue(Q); // 清理队列
printf("测试结束!\n");
}
/**测试模块**/

int main(void)
{
testLinkQueue();
return 0;
}