文章目录
前言
队列和栈一样,同样是操作受限的线性表,在日常生活中的体现也很多,所以学习队列也是必不可少的。本篇文章将会详细介绍队列的具体实现,去解释每行代码的意思,希望对你有所帮助。话不多说,直接上菜。
文章末尾附带源码。
一、初识队列
队列,顾名思义,和平时就会遇到的排队一样。那么排队有什么特点吗,我们都知道,排队就是为了 “先到先得” 先来排队的会先完成自己的事并第一个离开队伍。所以队列也一样,他的核心思想就是先进先出,先进的数据肯定是比后进的数据要先出的。栈跟队列是相反的,栈是 后进先出 的线性表,队列是 先进先出 的线性表,千万不要记混了。
一般规定在队尾插入,在队头删除,也就是说在队头出数据,在队尾入数据。队列同样可以使用顺序表或者链表实现,如果使用顺序表,也就是数组来实现,不管是在哪端插入删除,都无法避免将整个数组给挨个移动,效率比较低。如果使用链表的话,在哪端插入删除都有较好的解决方法,效率较高。所以本篇文章将会使用最常见的单链表的方式来实现队列。
二、头文件的编写
好的代码可读性是非常高的,分工明确,所以我们将库函数源文件、定义的结构体和函数声明放在一个头文件里面。我们创建一个头文件,叫做 “Queue.h” 。
1.引入库函数头文件
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
2.定义队列结构体
// 重定义队列的数据类型
typedef int QDataType;
// 队列元素的结构体
typedef struct QueueNode
{
// 队列元素的数据域
QDataType val;
// 队列元素的指针域
struct QueueNode* next;
}QNode;
我们使用 typedef 重定义队列的数据类型,后文的数据类型都被替换为 QDataType ,以方便以后改变数据类型时只需要改变此一行代码即可。由于我们是使用链表的方式来实现队列的,我们都知道,链表是通过指针来连接数据与数据的,所以在创建队列元素的结构体时,不仅仅需要存储元素的值,还需要一个指针来找到下一个元素。
3.定义传参结构体
// 传参结构体
typedef struct Queue
{
// 有一个指向队列的首元结点(队头)的指针
QNode* phead;
// 有一个指向队列的尾结点(队尾)的指针
QNode* ptail;
// 队列的元素个数
int size;
}Queue;
队列的基本特性就是在队尾插入,在队头删除,不会改变中间的值。所以队尾和队头是需要经常使用的,不妨我们直接记录一下队列的队尾和队头,那么找尾再也不用遍历链表了。于是我们创建一个结构体,这个结构体里面储存着队头和队尾的位置,还顺便储存着队列的大小。两个指针是指向队列元素的,可以通过指针创建队列元素,所有有了指向队头的头指针,就可以创建队列。这个结构体在后面会有大用。
4.声明功能函数
// 初始化队列
void QueueInit(Queue* pq);
// 销毁队列
void QueueDestroy(Queue* pq);
// 在队尾插入
void QueuePush(Queue* pq, QDataType x);
// 在队头弹出
void QueuePop(Queue* pq);
// 查询队头元素
QDataType QueueFront(Queue* pq);
// 查询队尾元素
QDataType QueueBack(Queue* pq);
// 判断队列是否为空
bool QueueEmpty(Queue* pq);
// 查询队列元素个数
int QueueSize(Queue* pq);
在一般情况下,对链表操作时,可能改变头指针的函数传参必须传入头指针的地址,通过二级指针来实现功能。但我们早在前面以及使用结构体包含了头指针以及尾指针,如果要改变头指针或者尾指针或者队列的元素个数,相当于在改变结构体,所以传入结构体的指针就能实现。
三、主函数文件的编写
同样的操作,我们将测试函数以及主函数放在同一个文件里面。我们创建一个源文件,叫做 “test.c” 。
1.包含头文件
#include"Queue.h"
2.编写测试函数
void test()
{
// 创建一个传参结构体成员
Queue q;
// 初始化这个成员
QueueInit(&q);
// 在队尾入队列
QueuePush(&q, 1);
// 在队尾入队列
QueuePush(&q, 2);
// 在队尾入队列
QueuePush(&q, 3);
// 在队尾入队列
QueuePush(&q, 4);
// 在队尾入队列
QueuePush(&q, 5);
// 如果队列不为空
while (!QueueEmpty(&q))
{
// 打印队头元素
printf("%d ", QueueFront(&q));
// 在队头出队列
QueuePop(&q);
}
printf("\n");
// 销毁
QueueDestroy(&q);
return 0;
}
测试用例仅作参考,可自拟测试用例。
四、功能函数的编写
我们创建一个源文件,叫做 “Queue.c” 。
1.包含头文件
#include"Queue.h"
2.初始化
// 传入传参结构体成员的地址
void QueueInit(Queue* pq)
{
// pq是指向传参结构体的指针,只要结构体创建了,那pq就不可能为空,为空说明传参错误或者结构体没创建
assert(pq);
// 让指向首元结点的头指针和指向尾结点的尾指针置空,说明队列为空
pq->phead = pq->ptail = NULL;
// 队列目前有效数据为0
pq->size = 0;
}
3.销毁
void QueueDestroy(Queue* pq)
{
// pq不可能为空,可以断言一下以免出现小差错
assert(pq);
// 如果pq指向的结构体里面的phead不为空,即存在结点
while (pq->phead)
{
// 创建一个临时指针变量指向首元结点
QNode* tmp = pq->phead;
// 让phead指向第二个结点
pq->phead = pq->phead->next;
// 释放首元结点空间
free(tmp);
// 释放空间后指针置空是好习惯
tmp = NULL;
}
// 最后没有结点的时候,把尾结点置空
pq->ptail = NULL;
// 让有效数据大小为0
pq->size = 0;
}
销毁队列的方式其实就是链表的头删,使用临时指针变量指向待销毁的空间,然后使原本指向该结点的指针指向下一个准备销毁的结点,最后销毁即可。在这里面,每销毁一个结点,头指针就会根据结点的指针域找到下一个结点的位置,销毁到最后一个结点时,头指针已经通过指针域被赋值为 NULL 了,由于销毁完了后,尾指针还指向原本尾结点的空间,数据个数也还没有改变,所以销毁的最后是需要把参数给修改回初始值的。
4.入队列
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
// 为新结点开辟空间
QNode* newNode = (QNode*)malloc(sizeof(QNode));
// 开辟失败时
if (newNode == NULL)
{
// 弹出反馈
perror("malloc fail");
// 终止程序
exit(-1);
}
// 为新结点赋值
newNode->val = x;
newNode->next = NULL;
// 当队列为空时
if (pq->ptail == NULL)
{
// 头指针指向新结点
pq->phead = pq->ptail = newNode;
}
// 当队列非空时
else
{
// 原尾结点的指针域指向新结点
pq->ptail->next = newNode;
// 尾指针指向新结点
pq->ptail = newNode;
}
// 队列的有效个数+1
++pq->size;
}
入队列操作只能是在队尾操作,我们可以通过尾指针去找到尾结点,然后在尾结点后面插入即可,需要注意的是,如果队列中没有数据的话,新结点就相当于即是尾结点又是首元结点,这个情况需要改变两个指针。
5.出队列
void QueuePop(Queue* pq)
{
assert(pq);
// 队列为空不能删除
assert(pq->phead);
// 临时指针指向首元结点
QNode* tmp = pq->phead;
// 当队列中只有一个结点时
if (pq->phead == pq->ptail)
{
// 将两个指针置空
pq->phead = pq->ptail = NULL;
}
// 当队列中不止一个结点时
else
{
// 让头结点指向第二个结点
pq->phead = pq->phead->next;
}
// 有效数据个数-1
--pq->size;
// 释放原首元结点的空间
free(tmp);
// 释放空间后指针置空
tmp = NULL;
}
出队列与入队列一样,也要判断边界情况,当队列中没有元素时,无法进行出队列操作,当队列中只有一个元素时,出的元素就是首元结点,出完后队列变成空队列。
6.查询队头数据
QDataType QueueFront(Queue* pq)
{
assert(pq);
// 队列不为空才有队头数据
assert(pq->phead);
// 根据头指针找到队头数据
return pq->phead->val;
}
由于我们的结构体里面包含了头指针,而头指针指向首元结点,首元结点就是队头数据,所以使用头指针便可找到队头数据。
7.查询队尾数据
QDataType QueueBack(Queue* pq)
{
assert(pq);
// 也是指队列不为空才能有队尾数据
assert(pq->ptail);
// 根据尾指针找到队尾数据
return pq->ptail->val;
}
结构体里面不仅仅包含了头指针,还包含了尾指针,尾指针指向尾结点,尾结点就是队尾数据,所以使用尾指针便可找到队尾数据。
8.判断队列是否为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
// 返回队列有效数据的个数是否等于0来判断队列是否为空
return pq->size == 0;
}
size 的值就是队列中的有效数据个数,如果队列中有效数据的个数为0,那就说明队列中无有效数据,就是空队列,反之就不是空队列。
9.查询队列中有效数据的个数
int QueueSize(Queue* pq)
{
assert(pq);
// 返回有效数据的个数
return pq->size;
}
我们在上面说过,size的值就是有效数据的个数,所以在这里直接返回size的值即可。
五、代码整合及结果演示
1.代码整合
若在整合后出现某些函数不安全的错误,请在头文件里面加上下面这行代码。
#define _CRT_SECURE_NO_WARNINGS 1
1.头文件 Queue.h 部分
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
// 队列的数据类型
typedef int QDataType;
// 队列的结构体
typedef struct QueueNode
{
// 队列的数据域
QDataType val;
// 队列的指针域
struct QueueNode* next;
}QNode;
// 传参结构体
typedef struct Queue
{
// 有一个指向队列的首元结点(队头)的指针
QNode* phead;
// 有一个指向队列的尾结点(队尾)的指针
QNode* ptail;
// 队列的元素个数
int size;
}Queue;
// 初始化队列
void QueueInit(Queue* pq);
// 销毁队列
void QueueDestroy(Queue* pq);
// 在队尾插入
void QueuePush(Queue* pq, QDataType x);
// 在队头弹出
void QueuePop(Queue* pq);
// 查询队头元素
QDataType QueueFront(Queue* pq);
// 查询队尾元素
QDataType QueueBack(Queue* pq);
// 判断队列是否为空
bool QueueEmpty(Queue* pq);
// 查询队列元素个数
int QueueSize(Queue* pq);
2.源文件 Queue.c 部分
#include"Queue.h"
void QueueInit(Queue* pq)
{
// pq是指向传参结构体的指针,不可能为空,为空说明传参错误
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
while (pq->phead)
{
QNode* tmp = pq->phead;
pq->phead = pq->phead->next;
free(tmp);
tmp = NULL;
}
pq->ptail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
// 为新结点开辟空间
QNode* newNode = (QNode*)malloc(sizeof(QNode));
if (newNode == NULL)
{
perror("malloc fail");
exit(-1);
}
// 为新结点赋值
newNode->val = x;
newNode->next = NULL;
// 当队列为空时
if (pq->ptail == NULL)
{
pq->phead = pq->ptail = newNode;
}
// 当队列非空时
else
{
pq->ptail->next = newNode;
pq->ptail = newNode;
}
++pq->size;
}
void QueuePop(Queue* pq)
{
assert(pq);
// 队列为空不能删除
assert(pq->phead);
QNode* tmp = pq->phead;
// 当队列中只有一个结点时
if (pq->phead == pq->ptail)
{
pq->phead = pq->ptail = NULL;
}
// 当队列中不止一个结点时
else
{
pq->phead = pq->phead->next;
}
--pq->size;
free(tmp);
tmp = NULL;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->ptail);
return pq->ptail->val;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
3.源文件 test.c 部分
#include"Queue.h"
void test()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePush(&q, 5);
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
printf("\n");
QueueDestroy(&q);
return 0;
}
int main()
{
test();
return 0;
}
2.结果演示
总结
本篇文章到这里就结束了,栈和队列部分其实还是比较简单的,总体难度不是很难。我只是使用了链表实现了队列,感兴趣的可以尝试使用顺序表来实现一下,相信对自己肯定会有帮助。
文章虽然比较简单,但是也并非绝对不可能出现错误,如果在文章中发现了错误,欢迎指正,谢谢。如果这篇文章对你有所帮助,别忘了三连博主,您的支持是我最大的鼓励,我会尽可能的去写出更加优质的文章。谢谢。