博主大二在读,希望和大家一起实现梦想,有一个美好的未来!!
目录
☄零、前言,外加动力源泉!!!
一、队列的定义
❄️二、队列的结构
☁️三、队列的实现
☀️ 3.1队列的结构创建
☀️ 3.2队列的接口函数
☀️ 3.3队列的初始化
☀️3.4队列的销毁
☀️ 3.5判断队列是否为空
☀️ 3.6入队列与出队列
☀️ 3.7获取队列中存储的个数
☀️ 3.8获取队列队头和队尾数据
四、完整代码
4.1 Queue.h
4.2Queue.c
4.3test.c
五、写在最后边的话
没错,在本博客里,美女是必不可少的,好不容易从小姑娘那里骗了张照片~
动力源泉啊兄弟们!!
趁热打铁,博主刚把队列吃透,为防止遗忘特在此总结了一篇博客。
本篇博客将会介绍队列的定义,队列的结构还有各个接口函数以及接口函数出现的意义,以及队列里边需要注意的点还有博主在学习队列中所出现的问题。
队列:只允许在一端进行插入数据,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)。
入队列:进行插入操作的一端称为队尾(跟单链表的尾插类似)
出队列:进行删除操作的一端称为队头(跟单链表的头删类似)
下图展示的是入队列,可以看出A先进,在队头
下边的图展示的是入队列,可以看出A也是先出去的,正对应了先入先出
对于队列,我们现在有两种选择,一种是数组,一种是链式结构
对于数组来说,如下图,入数据很简单,但是出数据呢,它需要把对头的数据删除了,然后其他的数据依次向前挪,这就显得比较繁琐了,所以我们这不考虑用数组
所以我们考虑用下边的链表形式,链表的话只要有头结点和尾结点的话就很容易的可以实现入队列和出队列,相当于头删尾插。
出数据也就是相当于头删很简单,就是头结点向前移动一位,然后free之前的头结点(head)。
而对于入数据也就是尾插,对于普通的单链表而言我们是需要找尾的,但是对于队列,尾的使用是常有的,每次找尾是很麻烦的吧,所以我们考虑定义一个尾指针tail,入队列更新尾结点就可以了。
分析结束那么就要开始定义队列的结构了
首先这是一个链表,我们需要一个链式结构
//队列 队尾进数据,队头出数据 , 先进先出
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QU;
这还没有完,对于以前的单链表来说我们 我们只需要定义一个头指针即可,因为那个链表尾指针的定义意义不大,尾删仍然需要遍历链表找尾部的前一个节点。
但是对于队列,我们需要的是最后一个结点,它的nexr存的就是入队列的数据,所以定义尾指针就很有必要了。
如果有多个值的话我们一般需要用结构体来定义,这时候就需要结构体typedef struct Queue{ }Queue;来引用头指针和尾指针。
typedef struct QUeueNode
{
QU* head;//头指针
QU* tail;//struct QueueNode* tail,尾指针
//int size;
}Queue;
里边的size是到时候用来确定队列的数据个数使用的,在 3.7我会提到
对于为何这样读者可能存疑,我来解释一下,其实QU*相当于上边next前的struct QueueNode*多以head,tail和next是相同类项的。
综上,队列的定义
typedef int QDataType;
//队列 队尾进数据,队头出数据 , 先进先出
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QU;
typedef struct QUeueNode
{
QU* head;//头指针
QU* tail;//struct QueueNode* tail,尾指针
//int size;
}Queue;
这个QDataType是宏定义的一种体现,如果中间的int变成char 的话,data就变成char类型的了。
因为传进去的实参不能为空,所以需要每个接口函数需要加个断言assert(pq),这里的pq是形参。
对于一个项目,一般我们是需要三个文件的,储存头文件的Queue.h,接口函数功能实现的Queue.c,以及测试函数结果的test.c测试函数
所以队列的结构完整是需要头文件的,具体看下方代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include//bool函数的库函数
#include
#include//断言的库函数,有了断言,对于bug的寻找很有用
#include
typedef int QDataType;
//队列 队尾进数据,队头出数据 , 先进先出
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QU;
typedef struct QUeueNode
{
QU* head;//头指针
QU* tail;//struct QueueNode* tail,尾指针
//int size;
}Queue;
队列各个功能的实现肯定不能只在一个文件里吧,那样会很乱,所以我们对于每个功能,创建一个接口函数,既方便调试,也方便功能的嵌套使用,也比较清晰。所有的接口函数以及具体功能如下,它的命名是根据库来命名的,建议读者根据我这个风格来命名,这样对以后c++的学习很有帮助。
void QueueInit(Queue* pq);//初始化
void QueuePush(Queue* pq,int x);//入队列
void QueuePop(Queue* pq);//出队列
void QueueDestroy(Queue* pq);//销毁链表,释放空间
QDataType QueueFrout(Queue* pq);//获取队头的数据,也可以说是头结点的数据
QDataType QueueBack(Queue* pq);//获取尾部的数据,也可以说尾结点的数据
int QueueSize(Queue* pq);//存储的数据个数
bool QueueEmpty(Queue* pq);//判断头结点是否为空,是的话返回真
void QueueInit(Queue* pq);//初始化头结点
最开始的head和tail应当为NULL,所以直接赋值空即可
void QueueInit(Queue* pq)//初始化
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
这里可以看出,pq是形参。
销毁也很简单但是很重要,所以我将其放在前边,初始化最前,销毁最后,读者务必不要忘记,因为不加的你的电脑可能会越来越卡的喔~
对于销毁,如同链表,需要从头到尾依次释放内存,但是这个结点掉的话如何找到下一个结点呢,所以这里需要在函数内部定义一个临时变量用来储存下一个结点的地址。当该变量为空NULL时则所有结点均被释放,我们可以用循环来表示,代码如下
void QueueDestroy(Queue* pq)//清理结点
{
assert(pq);
QU* cur = pq->head;
while (cur != NULL)//如果是cur->next那么尾结点无法处理
{
QU* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
看注释,是我犯的错误,这个循环条件不能是cur->next==NULL,因为这样的话最后一个结点会没有被释放,形成野指针。
因为出队列的话也就是头删,需要head不为NULL,而不少接口函数都需要确定head不为空才能运行,所以我们需要一个函数来判断head是否为空,head为NULL的话队列自然为空。
bool QueueEmpty(Queue* pq)//判断头结点是否为空,并给个返回值
{
assert(pq);
return pq->head == NULL;
}
这个代码的解释是如果pq->head!=NULL的话则返回假,所以我们用! QueueEmpty(Queue* pq)可以得到真条件,这一般加在断言里边,
assert(!QueueEmpty(pq));
入队列:
入队列就是尾插,分两种情况,第一种是pq->head和pq->tail均为NULL,这时候需要给他们两个都指向插入的节点
第二种是pq->head不为空,这时候不用动头结点head只用更新尾结点,使其指向新的尾结点就可。
具体如以下代码
void QueuePush(Queue* pq, int x)//入队列
{
assert(pq);
QU* newnode = (QU*)malloc(sizeof(QU));//malloc是为入队列的数据开辟一个新的结点
newnode->next = NULL;
newnode->data = x;
if (pq->head == NULL)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail ->next = newnode;
pq->tail = newnode;
}
}
出队列:
出队列也就是头删,首先需要头结点不为空,也需要传进去的结构体不为空
因为需要释放头结点还需要头结点更新它指向的地址,所以需要定义一个 nex指针来存放它的下一个数据。
void QueuePop(Queue* pq)//出队列
{
assert(pq);
assert(!QueueEmpty(pq));
QU* nex = pq->head->next;
free(pq->head);
pq->head = nex;
if (pq->head == NULL)
pq->tail = NULL;
}
我来解释一下pq->head->next的意思,因为pq里边存的有head和tail指针,而pq是结构体,结构体里边数据的使用需要 用->来引用所以pq->head相当于头结点pq->head->next相当于头结点的下一个位置。
这个有两种方法
第一种:在Queue里边定义一个size整数,每次入队列size++,出队列size--,最终的size就是队列中的数据个数。
第二种:遍历整个数组,每经过一个结点size++,直到空结束循环返回size。
这里博主用的第二种,因为结构体里不想定义太多东西,而且定义在结构体里的话,入队列和出队列也要多几行代码。
int QueueSize(Queue* pq)
{
QU* cur = pq->head;
int size=0;
while (cur)
{
++size;
cur = cur->next;
}
return size;
}
获取队头数据:
队头相当于链表的头结点,所以很简单,直接return就可,当然,断言得加
QDataType QueueFrout(Queue* pq)//获取数据
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
获取队尾数据:
队尾数据的获取如上,尾结点也有定义,直接使用就可。
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include//bool函数的库函数
#include
#include//断言的库函数,有了断言,对于bug的寻找很有用
#include
typedef int QDataType;
//队列 队尾进数据,队头出数据 , 先进先出
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QU;
typedef struct QUeueNode
{
QU* head;//头指针
QU* tail;//struct QueueNode* tail,尾指针
//int size;
}Queue;
void QueueInit(Queue* pq);//初始化
void QueuePush(Queue* pq,int x);//入队列
void QueuePop(Queue* pq);//出队列
void QueueDestroy(Queue* pq);//清理结点
QDataType QueueFrout(Queue* pq);//查找某个数据,也就是说获取
QDataType QueueBack(Queue* pq);//获取尾部的数据
int QueueSize(Queue* pq);//存储的数据个数
bool QueueEmpty(Queue* pq);//判断头结点是否为空,是的话返回真,
#include"Queue.h"
void QueueInit(Queue* pq)//初始化
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
void QueuePush(Queue* pq, int x)//入队列
{
assert(pq);
QU* newnode = (QU*)malloc(sizeof(QU));
newnode->next = NULL;
newnode->data = x;
if (pq->head == NULL)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail ->next = newnode;
pq->tail = newnode;
}
}
int QueueSize(Queue* pq)
{
QU* cur = pq->head;
int size=0;
while (cur)
{
++size;
cur = cur->next;
}
return size;
}
bool QueueEmpty(Queue* pq)//判断头结点是否为空,并给个返回值
{
assert(pq);
return pq->head == NULL;
}
void QueuePop(Queue* pq)//出队列
{
assert(pq);
assert(!QueueEmpty(pq));
QU* nex = pq->head->next;
free(pq->head);
pq->head = nex;
if (pq->head == NULL)
pq->tail = NULL;
}
void QueueDestroy(Queue* pq)//清理结点
{
assert(pq);
QU* cur = pq->head;
while (cur != NULL)//如果是cur->next那么尾结点无法处理
{
QU* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
QDataType QueueFrout(Queue* pq)//获取数据
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
博主测试的比较少,test()测试的是入队列出队列,还有队列的数据以及取出队列的元素。
test2()测试的是遍历队列,笔者可以研究为何这样。
#include"Queue.h"
void test1()
{
Queue qu;//定义一个队列
QueueInit(&qu);
QueuePush(&qu, 1);
QueuePush(&qu, 12);
QueuePush(&qu, 2);
printf("此时有%d个数据\n", QueueSize(&qu));
QueuePush(&qu, 3);
QueuePush(&qu, 4);
printf("%d->", QueueFrout(&qu));
QueuePop(&qu);
QueuePop(&qu);
printf("%d->", QueueFrout(&qu));
printf("\n");
printf("此时有%d个数据\n", QueueSize(&qu));
}
void test2()
{
Queue qu;
QueueInit(&qu);
QueuePush(&qu, 1);
QueuePush(&qu, 12);
QueuePush(&qu, 2);
while (!QueueEmpty(&qu))
{
QDataType front = QueueFrout(&qu);
printf("%d ", front);
QueuePop(&qu);
}
printf("\n");
}
int main()
{
//test1();
test2();
return 0;
}
队列的逻辑比较简单,烦琐的是它的结构,望读者把队列的结构搞懂,这样对于数据结构的学习也才算是开了个门,以后的代码也会很长,大家加油!!