个人技术分享

引言:于本篇博客当中,我们将讲到数据结构——队列的有关知识。而对于这次的队列,我们将会在单链表的基础上实现。

更多有关C语言和数据结构知识详解可前往个人主页:计信猫

一,队列的含义

        队列是一种特殊的线性表,它只允许在表的一端进行插入数据的操作,另一端进行删除数据的操作

队头:进行数据删除的一端

队尾:进行数据插入的一端

        所以,队列便可以用如下的图进行表示:

         而正因为队列遵循FIFO原则(即先进先出),使得入队列出队列的顺序一一对应,也就多被应用于广度优先遍历的操作。

二,队列结构体的定义

        老样子,我们仍然使用三文件操作法,分别创建Queue.h、Queue.c、test.c三个文件。

        因为先前提到,队列会在单链表的基础上实现,那么我们就需要先定义一个单链表结构体

typedef int QDataType;
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType val;
}QNode;

        但是,因为在队列之中,我们只需要关注队列队头队尾的两个节点即可,并且为了避免二级指针的使用难以理解,所以我们选择再定义一个结构体Queue用于储存队列队头队尾两节点

typedef struct Queue
{
	QNode* phead;
	QNode* ptail;
	int size;//记录队列中的节点个数
}Queue;

        包含可能用到的头文件后,我们的Queue.h的代码内容如下:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int QDataType;
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType val;
}QNode;
typedef struct Queue
{
	QNode* phead;
	QNode* ptail;
	int size;//记录队列中的节点个数
}Queue;

三,队列数据操作函数

1,队列的初始化

        队列的初始化其他数据结构的初始化别无它异,我们只需要将结构体中的指针类型初始化为NULL整型类型初始化为0就可以了。直接上代码:   

// 初始化队列 
void QueueInit(Queue* q)
{
	assert(q);
	q->phead = q->ptail = NULL;
	q->size = 0;
}

2,队列的尾插

        对于队列的尾插,其实也就和单链表的尾插一模一样。我们只需要使用malloc函数申请一个节点的空间,然后将新创建的节点连在队列的尾部,同时将ptail指针移到新形成的队尾,最后的最后,一定不要忘记size++。这样,一个队列的尾插函数就成功写出了。

// 队尾入队列 
void QueuePush(Queue* q, QDataType data)
{
	QNode* newnode = (QNode*)malloc(sizeof(QNode));//创建新节点
	if (newnode == NULL)
	{
		perror("malloc fail!");
		return;
	}
	newnode->next = NULL;
	newnode->val = data;
	//队列元素为0个时
	if (q->ptail == NULL)
	{
		q->phead = q->ptail = newnode;
	}
	//队列有元素时
	else
	{
		q->ptail->next = newnode;
		q->ptail = newnode;
	}
	q->size++;
}

3,队列的头删

        因为队列只可以在队头进行数据删除,所以队列就只有一个头删函数

        如上图所示,对于队列的头删函数,我们需要首先创建节点结构体指针next记录队头的下一个节点的地址,以免队头free掉之后导致的后续节点的丢失,然后我们再将phead指针赋值为next,最后再进行size--即可

        但是有一个特殊情况,当队列只有一个节点的时候,我们进行队列的头删之后,ptail则会变成野指针,这时候我们就需要特殊处理,将ptail也赋值为NULL,防止野指针的出现。如图:

         综上所述,我们的队列头删函数如下: 

// 队头出队列 
void QueuePop(Queue* q)
{
	assert(q);
	assert(q->size > 0);
	QNode* next = q->phead->next;
	free(q->phead);
	q->phead = next;
	//特殊情况:队列只有一个节点的时候
	if (q->phead == NULL)
	{
		q->ptail = NULL;
	}
	//勿忘size--
	q->size--;
}

4,获取队列头部数据

        这时候,我们所创建的Queue结构体就派上大用场了,因为这个结构体直接就保存了我们的phead节点,所以我们直接使用就可以了。代码如下:

// 获取队列头部元素 
QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(q->phead);
	return q->phead->val;
}

5,获取队列尾部数据 

        那么这个函数,也就跟前一个函数相差不大了。都是直接使用Queue结构体就可以了,我们直接上代码:

// 获取队列队尾元素 
QDataType QueueBack(Queue* q)
{
	assert(q);
	assert(q->ptail);
	return q->ptail->val;
}

6,获取队列有效元素个数 

        在Queue结构体中,我们早就使用了size来记录有效元素的个数,所以我们直接返回size的值即可。

// 获取队列中有效元素个数 
int QueueSize(Queue* q)
{
	assert(q);
	return q->size;
}

7,判断队列是否为空 

        该函数的返回值为布尔类型,即truefalse

队列为空,返回:true

队列不为空,返回:false

        对于队列是否为空,我们只需要判断size是否为零就可以了。所以代码如下:

// 检测队列是否为空
bool QueueEmpty(Queue* q)
{
	assert(q);
	return q->size == 0;
	//若size等于零,则表达式为真,则队列为空
	//若size不等于零,则表达式为假,则队列为假
}

8,队列的销毁 

         因为我们创建队列时使用了malloc函数,故为了防止内存溢出,我们需要创建一个队列的销毁函数,来完成释放队列申请的空间的操作。

        在该函数中,我们需要再创建一个QNode结构体指针cur用于保存即将被释放的节点的下一个节点的地址,以防止地址的丢失,然后再使用free函数释放掉节点即可。最后的最后,勿忘将size置为零

        所以我们的代码如下:

// 销毁队列 
void QueueDestroy(Queue* q)
{
	assert(q);
	QNode* next = q->phead;
	while (next)
	{
		QNode* cur = next->next;
		free(next);
		cur = next;
	}
	q->size = 0;
}