从零开始学数据结构与算法(三) :队列

一、队列的逻辑结构

1.队列的定义

队列是一种线性数据结构,具有先进先出(FIFO)的特点,即首先进入队列的元素首先被处理,最后进入队列的元素最后被处理。队列通常包含两个基本操作:入队(enqueue)和出队(dequeue)。入队操作将元素添加到队列的尾部,出队操作则将队列头部的元素删除并返回其值。队列常用于处理需要按顺序执行的任务,例如打印作业、消息传递等场景。
在这里插入图片描述

2.队列的抽象数据类型定义

ADT Queue
DataModel
	队列中元素具有先进先出特性,相邻元素具有前驱和后继关系
Operation
	InitQueue
		输入:无
		功能:队列的初始化
		输出:一个空队列
	DestroyQueue
		输入:无
		功能:队列的销毁
		输出:释放队列所占用的储存空间
	Enqueue
		输入:元素值x
		功能:入队操作,在队尾插入元素x
		输出:如果插入成功,队尾增加一个元素,否则返回插入失败信息
	DeQueue
		输入:无
		功能:出队操作,删除队尾元素
		输出:如果删除成功,队头减少一个元素,否则返回删除失败信息
	GetHead
		输入:无
		功能:读取队头元素
		输出:若队列不为空,返回队头元素
	Empty
		输入:无
		功能:判空操作,判断队列是否为空
		输出:如果队列为空,返回1,否则返回0
endADT

二、队列的顺序存储结构及实现

1.顺序队列

队列的顺序存储结构叫做顺序队列(sequential queue)
正常队列如果要通过数组来实现的话,入队操作相当于在数组中追加,时间复杂度为O(1),但出队操作十分麻烦,每一次都需要将n-1个元素前移一位,时间复杂度为O(n)。如下图所示:
在这里插入图片描述
那有没有什么方法可以使入队出队操作的时间复杂度都为O(1)呢?

有的,如图所示,使用rearfront两个指针控制队头队尾,就可以避免移动元素,但此时队列的队头队尾都是活动的。这里进行一个规定,front表示队头的前一个位置,rear表示队尾位置。其实这个规定也不是死的,但一定是最好的,可以省去一些不必要的边界判断。

2.循环队列的存储结构定义

在上述的顺序队列中,随着队列的入队和出队,整个队列向着数组中下标较大的位置移动过去,从而产生了队列的单向移动性。当元素被插入数组下标较大的位置之后,数组空间就用尽了,尽管此时数组的前端还有空闲空间,这种现象叫做假溢出。

解决假溢出的方法是把存储队列的数组看成是头尾相接的循环结构,即允许从数组中下标最大的位置延续到下标最小的位置。队列这种头尾相接的顺序存储结构称为循环队列(circular queue)。
下面是循环队列的存储结构定义:

#define QueueSize 100 //数组最大长度

typedef int DataType; 

typedef struct CirQueue
{
	DataType Data[QueueSize];
	int front, rear;
}CirQueue;

3.循环队列的实现

·循环队列的初始化

初始化只需要将队头和队尾指向数组的某一个位置,一般是数组的高端,代码实现如下:

void InitQueue(CirQueue * Q)
{
	Q->front = Q->rear = QueueSize - 1;
}

·循环队列的销毁

循环队列是静态存储分配,在循环队列变量退出作用域时自动释放所占内存单元,因此循环队列无须销毁。

·入队操作

循环队列的入队只需要将队尾位置rear在循环意义下加1,然后将待插入元素x插入队尾位置。函数EnQueue的返回值表示入队操作是否正确执行,代码实现如下:

int EnQueue(CirQueue* Q, DataType x)
{
	if ((Q->rear + 1) % QueueSize == Q->front)
	{
		printf("上溢错误,插入失败\n");
		return 0;
	}
	Q->rear = (Q->rear + 1) % QueueSize; //队尾位置在循环意义下加1
	Q->Data[Q->rear] = x; //在队尾插入元素
	return 1;
}

·出队操作

循环队列的出队操作只需要将队头位置front在循环意义下加1,然后读取并返回队头元素。函数DeQueue的返回值表示出队操作是否正确执行,被删除操作通过指针参数ptr返回,代码实现如下:

int DeQueue(CirQueue* Q, DataType *ptr)
{
	if (Q->rear == Q->front)
	{
		printf("下溢错误,删除失败\n");
		return 0;
	}
	Q->front = (Q->front + 1) % QueueSize; //队尾位置在循环意义下加1
	*ptr = Q->Data[Q->front];
	return 1;
}

·取队头元素

去队头元素和出队操作类似,唯一的区别是不改变队头位置,代码实现如下:

int GetHead(CirQueue* Q, DataType* ptr)
{
	if (Q->rear == Q->front)
	{
		printf("下溢错误,取队头失败\n");
		return 0;
	}
	int i = (Q->front + 1) % QueueSize; //不改变队头位置
	*ptr = Q->Data[i];
	return 1;
}

·判空操作

循环队列的判空操作只需要判断front是否等于rear,代码如下:

int Empty(CirQueue* Q)
{
	if (Q->rear == Q->front) return 1; //队空返回1
	else return 0;
}

4.循环队列的使用

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

//将循环队列的存储结构定义和每个函数定义放到这里

int main()
{
	DataType x;
	CirQueue Q;
	InitQueue(&Q);
	printf("对5和8执行入队操作,");
	EnQueue(&Q, 5);
	EnQueue(&Q, 8);
	if (GetHead(&Q, &x) == 1)
	{
		printf("当前队头元素为:%d\n", x); //当前队头元素为:5
	}
	if (DeQueue(&Q, &x) == 1)
	{
		printf("执行一次出队操作,出队元素为%d\n", x); //执行一次出队操作,出队元素为5
	}
	if (GetHead(&Q, &x) == 1)
	{
		printf("当前队头元素为:%d\n", x); //当前队头元素为:8
	}
	printf("请输入入队元素:");
	scanf("%d", &x);
	EnQueue(&Q, x);
	if (Empty(&Q))
	{
		printf("队列为空\n");
	}
	else
	{
		printf("队列不为空\n"); //队列有两个元素,输出队列非空
	}

	return 0;
}

三、队列的链式存储结构及实现

1.链队列的存储结构定义

队列的链式存储结构被称为链队列(linked queue),通常用单链表来表示,其结点结构与单链表结点结构相同。为了使空队列和非空队列的操作一致,链队列也要加上头结点。根据队列先进先出的特性,为了操作上的方便,设置队头指针指向链队列的头结点,队尾指针指向终端结点。
下面给出链队列的存储结构定义:

typedef int DataType;

typedef struct Node
{
	DataType data;
	struct Node * next;
}Node;

typedef struct
{
	Node* front, * rear;
}LinkQueue;

2.链队列的实现

·链队列的初始化

初始化链队列只需申请头结点,然后让队头指针和队尾指针均指向头结点,代码如下:

void InitQueue(LinkQueue* Q)
{
	Node* s = (Node*)malloc(sizeof Node);
	Q->front = Q->rear = s; //队头指针和队尾指针均指向头结点
}

·链队列的销毁

链队列是动态分配内存,需要释放链队列的所有结点的存储空间,代码如下:

void DestroyQueue(LinkQueue* Q)
{
	Node* p = Q->front, *temp;
	while (p != NULL)
	{
		temp = p->next;
		free(p);
		p = temp;
	}
}

·入队操作

链队列的插入只考虑在链表的尾部进行,由于链队列带头结点,空队列和非空队列插入语句一致,代码实现如下:

void EnQueue(LinkQueue* Q, DataType x)
{
	Node* s = (Node*)malloc(sizeof Node);
	s->data = x;
	s->next = NULL;
	Q->rear->next = s; Q->rear = s;
}

·出队操作

链队列的删除只考虑在链表的头部进行,注意队列长度为1的特殊情况,特殊情况处理如图所示:
在这里插入图片描述
函数DeQueue表示出队操作是否正确执行,如果正确出队,被删元素通过指针参数ptr返回,代码实现如下:

int DeQueue(LinkQueue* Q, DataType* ptr)
{
	Node* p;
	if (Q->rear == Q->front)
	{
		printf("下溢错误,删除失败\n");
		return 0;
	}
	p = Q->front->next;
	*ptr = p->data;
	Q->front->next = p->next;
	if (p->next == NULL)
	{
		Q->rear = Q->front;
	}
	free(p);
	return 1;
}

·取队头元素

返回第一个元素结点的数据域,代码如下:

int GetHead(LinkQueue* Q, DataType* ptr)
{
	Node* p = NULL;
	if (Q->rear == Q->front)
	{
		printf("下溢错误,删除失败\n");
		return 0;
	}
	p = Q->front->next;
	*ptr = p->data;
	return 1;
}

·判空操作

链队列的判空操作只需要判断front是否等于rear,代码如下:

int Empty(LinkQueue* Q)
{
	if (Q->front == Q->rear) return 1;
	return 0;
}

3.链队列的使用

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

//将队列存储结构定义和各个函数定义放到此处

int main()
{
	DataType x;
	LinkQueue Q;                   /*定义结构体变量Q为链队列类型*/
	InitQueue(&Q);                  /*初始化链队列Q*/
	printf("对5和8执行入队操作,");
	EnQueue(&Q, 5);
	EnQueue(&Q, 8);
	if (GetHead(&Q, &x) == 1)
		printf("当前队头元素为:%d\n", x);              /*输出当前队头元素5*/
	if (DeQueue(&Q, &x) == 1)
		printf("执行一次出队操作,出队元素是:%d\n", x);   /*输出出队元素5*/
	if (GetHead(&Q, &x) == 1)
		printf("当前队头元素为:%d\n", x);             /*输出当前队头元素8*/
	printf("请输入入队元素:");
	scanf("%d", &x);
	EnQueue(&Q, 5);
	if (Empty(&Q) == 1)
		printf("队列为空\n");
	else
		printf("队列非空\n");             /*队列有2个元素,输出队列非空*/
	DestroyQueue(&Q);
	return 0;
}

四、循环队列和链队列的比较

循环队列和链队列是两种常见的队列实现方式。它们有一些相同点,如队列的基本操作都是入队和出队;也有一些不同点,如实现方式、空间利用效率、时间复杂度等。

以下是循环队列和链队列的比较:

  1. 实现方式:循环队列使用数组实现,链队列使用链表实现。

  2. 空间利用效率:循环队列的空间利用效率较高,因为当队列满时,只需要将队头和队尾指针相加再取余即可判断队列是否已满,而不需要额外的空间来判断;链队列的空间利用效率相对较低,因为每一个元素都需要一个结点来存储。

  3. 时间复杂度:循环队列和链队列的基本操作时间复杂度都是O(1),即常数级别的时间复杂度。但在某些情况下,循环队列的入队和出队操作可能需要对数组进行复制移动,时间复杂度为O(n)。而链队列的插入和删除操作不需要复制移动,所以基本操作的时间复杂度始终为O(1)。

  4. 可扩展性:由于循环队列使用数组实现,所以队列的容量固定;而链队列的容量可以根据需要进行动态调整。

综上所述,循环队列和链队列各有优缺点,选择哪种队列实现方式需要根据实际情况来确定。如果需要高效地使用空间且队列大小不变,可以选择循环队列;如果队列大小不确定或经常需要动态调整,可以选择链队列。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Pigwantofly

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值