Bootstrap

【初阶数据结构与算法】线性表之队列的定义与实现

在这里插入图片描述

一、队列的概念与结构

1. 概念

   队列是一种只允许在⼀端进⾏插⼊数据操作,在另⼀端进⾏删除数据操作的特殊线性表,它具有先进先出FIFO(后进后出)的特性
   用来插入数据的那一端称为队尾,用来删除数据的一端则称为对头,插入数据被称为入队列,删除数据被称为出队列,队列的大致示意图如下:
在这里插入图片描述
   队列的数据从队尾插入,从队头删除,根据上图的示意,我们就可以发现,A先进入队列,B后进入队列,那么在出队的时候,就还是A先出,B后出,所以队列确实满足先进先出,后进后出的特性

2.队列结构定义

   队列和栈一样既可以选择数组做底层,也可以选择链表作为它的底层,那么哪一个更好呢?这个就需要我们去分析一下了
   如果采用数组,那么我们入队列就可以直接在数组最后添加数据,时间复杂度为O(1),比较方便,但是当我们要从数组头删数据就比较麻烦了,因为数组头删的时间复杂度为O(N),效率并不是很高
   我们再来分析一下使用链表怎么样,如果我们使用链表实现队列,那么在头部删除没有问题了,时间复杂度为O(1),但是我们要在最后插入数据,对于链表来说,尾插的时间复杂度是O(N),那么到底选哪一个才好?
   这里就不卖关子了,我们队列这个结构最好使用链表完成,我们要知道为什么链表尾插的时间复杂度为O(N),因为我们找不到尾结点,需要循环遍历链表找到尾结点才能尾插,那么我们能不能想办法直接找到尾结点呢
   当然可以,链表不好的地方就是不方便找尾结点,既然知道了不足,我们在定义队列时就可以这样定义:让队列里面包含两个指针,分别指向链表的头和尾节点,反正队列只需要操作队头和队尾的数据
   我们拿到队尾和队头的节点后基本上就可以完成队列的操作了,其它节点我们并不关心,但是由于队列是建立在链表结构之上,所以我们要先定义一个类似于链表节点的队列节点结构,然后才能让队列指向它的头和尾,具体结构如下:

typedef int QDataType;

//队列中一个节点的定义
//看起来和链表差不多
//但是要注意这里它已经变成队列的节点了
typedef struct QueueNode
{
	QDataType data;
	struct QueueNode* next;
}QueueNode;

//真正使用的对列的定义
typedef struct Queue
{
    //指向队头的指针
	QueueNode* phead;
	//指向队尾的指针
	QueueNode* ptail;
}Queue;

   在上面的队列结构里,只有两个指向队列头和尾的指针,整个队列的操作就可以基本上只通过这两个指针来操作了,我们队列节点就是仿造链表节点来进行定义的,所以可以说我们队列的底层结构还是链表,只是对它进行封装后就变成了我们的队列
   那么这样的结构是否完美了呢?既然我们要实现一个队列,就应该尽可能的想到它的更多应用场景,如果我们需要计算这个队列的长度,是不是就会很麻烦,又要遍历整个队列才能得出结果,时间复杂度为O(N)
   所以为了优化这一点,在队列结构中做了优化,我们加入了一个新的成员,它用来记录队列的长度,而队列节点结构的定义不变,如下:

typedef int QDataType;

//队列一个节点的定义
typedef struct QueueNode
{
	QueueDataType data;
	struct QueueNode* next;
}QueueNode;

//真正使用的对列的定义
typedef struct Queue
{
    //用来记录队列的长度
    int size; 
    //指向队头的指针
	QueueNode* phead;
	//指向队尾的指针
	QueueNode* ptail;
}Queue;

   如果对链表还不熟悉的话,推荐先学习链表:【初阶数据结构与算法】线性表之单链表的定义与实现

二、队列的实现

1.队列的初始化和销毁

初始化

   队列里面保存的是指向队头和队尾的指针,以及保存队列的长度的size,分别置空即可,如下:

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

销毁

   队列的销毁也很简单,只需要通过队列中存储的头指针,遍历销毁整个队列即可,最后将存储的指针和队列大小置空,如下:

//队列销毁
void QueueDestroy(Queue* pq)
{
	assert(pq);
	QueueNode* pcur = pq->phead;
	while (pcur)
	{
		QueueNode* del = pcur;
		pcur = pcur->next;
		free(del);
	}
	pq->phead = pq->ptail = NULL;
	pq->size = 0;
}

2.队列的节点申请和入队列操作

队列的节点申请

   由于我们入队列必须要申请节点,所以我们将其封装为一个函数,队列的节点申请和链表的节点申请类似,直接malloc一个节点大小的空间出来,然后根据给出的数据简单初始化一下这个节点即可,但是不要忘记了判断malloc返回值,以免节点申请失败,如下:

//队列节点申请
QueueNode* QueueBuyNode(QDataType x)
{
	QueueNode* newnode = malloc(sizeof(QueueNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

入队列

   解决了队列节点的申请后,我们就可以直接来实现入队列的操作了,由于我们队列结构中已经有了尾结点的地址,所以我们可以直接让尾结点的next指针指向新节点,然后让队列中保存的尾结点更新一下
   但是我们要注意一下,我们在第一次入队列时,队列为空,那么尾结点自然也就不存在,此时按照上面的步骤去操作就要出错,所以我们最好判断一下,如果队列为空,直接让头和尾指向新节点,让size++,否则就按照上面的逻辑走,如下:

//入队列
void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);
	QueueNode* newnode = QueueBuyNode(x);
	if (pq->phead = NULL)
	{
		pq->phead = pq->ptail = newnode;
		pq->size++;
		return;
	}
	pq->ptail->next = newnode;
	pq->ptail = newnode;
	pq->size++;
}

3.队列的判空和出队列操作

队列的判空

   我们要出队列就一定要保证队列中有数据,所以我们先来实现队列的判空,队列的判空也非常简单,只需要判断队列中的phead是否为空即可,如下:

//队列判空
bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->phead == NULL;
}

出队列

   有了队列判空之后,我们就可以在出队列前判断一下队列是否为空,不为空我们才继续进行出队列操作,我们之前说过,出队列是在删除队列的头
   由于我们队列结构中有队列的头,所以删除队列的头结点也很简单,可以先创建一个队列节点来保存头结点的下一个节点,然后直接释放掉头结点,释放掉之后让pq中的头指针重新指向保存的节点
   当然,出队列说明队列的有效节点个数要少一个了,所以不要忘了让size- -,如下:

//出队列
void QueuePop(Queue* pq)
{
	assert(!QueueEmpty(pq));
	QueueNode* next = pq->phead->next;
	free(pq->phead);
	pq->phead = next;
	pq->size--;
}

4.取队头和队尾元素

取队头元素

   取队头元素就是取出队列中第一个元素,直接返回phead中的元素即可,如下:

//取队头数据
QDataType QueueFront(Queue* pq)
{
	assert(pq);
	return pq->phead->data;
}

取队尾元素

   取队尾元素就是取出队列中最后一个元素,直接返回ptail中的元素即可,如下:

//取队尾数据
QDataType QueueBack(Queue* pq)
{
	assert(pq);
	return pq->ptail->data;
}

5.获取队列有效节点个数

   我们队列有效元素个数一直是size在记录,直接将其返回就可以了,如下:

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

三、队列源码

#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
{
	int size;
	QueueNode* phead;
	QueueNode* ptail;
}Queue;

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

//队列销毁
void QueueDestroy(Queue* pq)
{
	assert(pq);
	QueueNode* pcur = pq->phead;
	while (pcur)
	{
		QueueNode* del = pcur;
		pcur = pcur->next;
		free(del);
	}
	pq->phead = pq->ptail = NULL;
	pq->size = 0;
}

//队列节点申请
QueueNode* QueueBuyNode(QDataType x)
{
	QueueNode* newnode = malloc(sizeof(QueueNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

//入队列
void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);
	QueueNode* newnode = QueueBuyNode(x);
	if (pq->phead == NULL)
	{
		pq->phead = pq->ptail = newnode;
		pq->size++;
		return;
	}
	pq->ptail->next = newnode;
	pq->ptail = newnode;
	pq->size++;
}

//队列判空
bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->phead == NULL;
}

//出队列
void QueuePop(Queue* pq)
{
	assert(!QueueEmpty(pq));
	QueueNode* next = pq->phead->next;
	free(pq->phead);
	pq->phead = next;
	pq->size--;
}

//取队头数据
QDataType QueueFront(Queue* pq)
{
	assert(pq);
	return pq->phead->data;
}

//取队尾数据
QDataType QueueBack(Queue* pq)
{
	assert(pq);
	return pq->ptail->data;
}

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

   那么今天关于队列的定义和实现就分享到这里,有什么疑问欢迎提出,下一篇我将会带大家做几道关于栈和队列的练习,随后就进入二叉树的学习,小小期待一下吧!
   bye~

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;