【数据结构与算法】使用单链表实现队列:原理、步骤与应用

   

            💓 博客主页:倔强的石头的CSDN主页 

           📝Gitee主页:倔强的石头的gitee主页

            ⏩ 文章专栏:《数据结构与算法》

                                  期待您的关注

1b7335aca73b41609b7f05d1d366f476.gif

目录

一、引言

🎄队列的概念

🎄为什么要用单链表实现队列

二、单链表前情回顾

三、队列的结构定义

🍃单个元素的结构定义

🍃队列的结构定义

🍃图解单链表与队列的关系

四、队列的接口实现

🌾初始化

🌾销毁

🌾入队列(队尾插入)

🌾出队列(队头删除)

🌾获取队首元素

🌾获取队尾元素

🌾获取队列元素个数

🌾队列判空

五、C语言实现代码

Queue.h   队列头文件

Queue.c    队列实现文件

test.c        main函数测试文件

测试结果

六、应用场景

七、总结


一、引言

🎄队列的概念

队列(Queue)是一种特殊类型的线性数据结构,它遵循特定的操作顺序。队列的基本操作通常是在一端添加元素(称为入队或enqueue),在另一端移除元素(称为出队或dequeue)。这种操作特性使得队列符合“先进先出”(FIFO, First In First Out)的原则。

基本概念

  1. 先进先出(FIFO)原则:这是队列的核心特性。它意味着最早被添加到队列中的元素将是第一个被移除的元素。这个原则确保了数据处理的顺序性。

  2. 队头(Front):队列中第一个被添加的元素位于队头,但它不是永远位于队列的第一个位置,而是指按照入队顺序,最先应该被出队的元素的位置。在出队操作中,总是从队头移除元素。

  3. 队尾(Rear)或队末:新元素总是被添加到队列的这一端。在入队操作中,新元素总是被放置在队尾。

  4. 队列为空:当队列中没有元素时,称队列为空队列。

  5. 队列满:在某些实现中,特别是使用静态数组实现的队列,当队列无法再添加新元素时,称为队列满。但在使用链表实现的队列中,通常不会遇到队列满的情况,因为链表可以动态扩展。

队列提供了一种有效的方式来管理和处理需要按照特定顺序执行的任务或数据项。通过使用队列,可以确保数据项按照它们被接收或生成的顺序进行处理,这是许多应用中非常关键的要求。

🎄为什么要用单链表实现队列

  1. 动态内存分配
    单链表节点是动态分配的,这意味着队列的大小可以根据需要动态地增长和缩小。与静态数组实现的队列不同,单链表队列不需要预先定义最大的容量,从而避免了因队列容量不足而导致的内存溢出问题。

  2. 高效操作
    在单链表队列中,入队(enqueue)操作通常只需要在链表尾部添加一个节点,时间复杂度为O(1)。出队(dequeue)操作也只需要删除链表头部的节点,时间复杂度同样为O(1)。这使得单链表队列在处理大量数据时具有较高的效率。而数组不方便头插或头删,不管将数组的首部当作队首还是队尾都会降低效率

  3. 内存利用率
    单链表队列在添加和删除元素时,只需要分配或释放单个节点的内存,而不需要像数组那样可能需要分配或释放整个数据块的内存。这有助于减少内存碎片,提高内存利用率。另外队列只需要对首尾元素进行操作,带尾指针的单链表已经足够高效的进行插入删除,相比双向链表节省了一半的指针空间。

  4. 灵活性
    单链表队列在结构上相对灵活,可以根据具体需求进行扩展或修改。例如,可以在节点中添加额外的信息以支持更复杂的操作,或者在链表中插入或删除节点以实现特定的功能。

  5. 易于实现
    单链表的基本操作(如添加节点、删除节点等)相对简单,因此使用单链表实现队列也相对容易。这使得初学者能够更容易地理解和实现队列数据结构。

  6. 适应性强
    在实际应用中,队列可能会遇到各种复杂的情况,如并发访问、数据异常等。单链表队列由于其动态性和灵活性,能够较好地适应这些复杂情况。例如,在并发环境下,可以使用锁或其他同步机制来确保队列操作的原子性;在数据异常时,可以通过遍历链表来检查和处理异常情况。

二、单链表前情回顾

参考文章:

【C语言项目实战】使用单链表实现通讯录-CSDN博客

三、队列的结构定义

🍃单个元素的结构定义

  • 数据部分
  • 指向下一个元素的指针next
// 链式结构:表示队列 
typedef int QDataType;
typedef struct QListNode
{
	struct QListNode* next;
	QDataType data;
}QNode;

🍃队列的结构定义

  • 指向队首元素的指针phead
  • 指向队尾元素的指针ptail
  • 队列的元素个数size
// 队列的结构 
typedef struct Queue
{
	QNode* head;
	QNode* tail;
	int size;
}Queue;

将队列的首尾指针封装成一个结构体,可以方便函数调用,统一接口
另外使用一个整型变量记录元素个数,利于其他函数功能实现

🍃图解单链表与队列的关系

 

四、队列的接口实现

🌾初始化

  • 对形参判空(接收的地址必须是有效的(队列必须存在)
  • 队列的首尾指针初始化为NULL
  • size变量初始化为0
// 初始化队列 
void QueueInit(Queue* q)
{
	assert(q);//接收的地址必须是有效的(队列必须存在)
	q->head = q->tail = NULL;
	q->size = 0;
}

🌾销毁

  • 对形参判空
  • 创建指针循环遍历链表:     每次记录下指针的下一个节点,释放指针指向的节点,指针指向下一个节点
  • 出循环后,将首尾指针指向NULL
  • size置0
// 销毁队列 
void QueueDestroy(Queue* q)
{
	assert(q);
	while (q->head)//释放所有节点
	{
		QNode* next = q->head->next;
		free(q->head);
		q->head = next;
	}
	q->head = q->tail = NULL;
	q->size = 0;
}

🌾入队列(队尾插入)

  • 接收两个参数,队列的地址和要插入的数据
  • 首先,对形参指针判空
  • 然后申请新节点,next指针指向NULL,数据部分为要插入的数据
  • 接下来,对空队列和非空队列分别处理:
  • 空队列直接让首尾指针都指向新节点
  • 非空队列:尾指针指向节点的next指针指向新节点,尾指针再指向新节点
  • 完成插入之后,size++
// 队尾入队列 
void QueuePush(Queue* q, QDataType data)
{
	assert(q);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)//判定是否申请成功
	{
		perror("newnode error\n");
		exit(1);
	}
	newnode->data = data;
	newnode->next = NULL;

	if (q->head == NULL)//对空队列入队的处理
	{
		q->head = q->tail = newnode;
	}
	else               //对非空队列入队的处理
	{
		q->tail->next = newnode;
		q->tail = newnode;
	}
	q->size++;
}

🌾出队列(队头删除)

  • 对形参判空
  • 对队列判空
  • 队首删除分两种情况
  • 队列只有一个元素的情况:    释放该元素空间,首尾指针都指向NULL
  • 队列有多个元素的情况:    记录下第二个节点地址,释放首节点,首指针指向第二个节点
  • 删除完节点之后,size--
// 队头出队列 
void QueuePop(Queue* q)
{
	assert(q);
	assert(q->head);//队列不能为空
	if (q->head == q->tail)//对只有一个元素的队列的出队处理
	{
		free(q->head);
		q->head = q->tail = NULL;
	}
	else          //对存在多个元素的队列的出队处理
	{
		QNode* next = q->head->next;
		free(q->head);
		q->head = next;
	}
	q->size--;
}

🌾获取队首元素

  • 形参判空
  • 队列判空
  • 返回队首元素的数据
// 获取队列头部元素 
QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(q->head);//队列不能为空
	return q->head->data;
}

🌾获取队尾元素

  • 形参判空
  • 队列判空
  • 返回队尾元素的数据
// 获取队列队尾元素 
QDataType QueueBack(Queue* q)
{
	assert(q);
	assert(q->head);
	return q->tail->data;
}

🌾获取队列元素个数

  • 形参判空
  • 返回size
// 获取队列中有效元素个数 
int QueueSize(Queue* q)
{
	assert(q);
	return q->size;
}

🌾队列判空

  • 形参判空
  • 返回size==0的结果
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q)
{
	assert(q);
	return q->size == 0;
}

五、C语言实现代码

Queue.h   队列头文件

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

// 链式结构:表示队列 
typedef int QDataType;
typedef struct QListNode
{
	struct QListNode* next;
	QDataType data;
}QNode;

// 队列的结构 
typedef struct Queue
{
	QNode* head;
	QNode* tail;
	int size;
}Queue;

// 初始化队列 
void QueueInit(Queue* q);
// 队尾入队列 
void QueuePush(Queue* q, QDataType data);
// 队头出队列 
void QueuePop(Queue* q);
// 获取队列头部元素 
QDataType QueueFront(Queue* q);
// 获取队列队尾元素 
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数 
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q);
// 销毁队列 
void QueueDestroy(Queue* q);

Queue.c    队列实现文件

#include"Queue.h"

// 初始化队列 
void QueueInit(Queue* q)
{
	assert(q);//接收的地址必须是有效的(队列必须存在)
	q->head = q->tail = NULL;
	q->size = 0;
}

// 队尾入队列 
void QueuePush(Queue* q, QDataType data)
{
	assert(q);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)//判定是否申请成功
	{
		perror("newnode error\n");
		exit(1);
	}
	newnode->data = data;
	newnode->next = NULL;

	if (q->head == NULL)//对空队列入队的处理
	{
		q->head = q->tail = newnode;
	}
	else               //对非空队列入队的处理
	{
		q->tail->next = newnode;
		q->tail = newnode;
	}
	q->size++;
}

// 队头出队列 
void QueuePop(Queue* q)
{
	assert(q);
	assert(q->head);//队列不能为空
	if (q->head == q->tail)//对只有一个元素的队列的出队处理
	{
		free(q->head);
		q->head = q->tail = NULL;
	}
	else          //对存在多个元素的队列的出队处理
	{
		QNode* next = q->head->next;
		free(q->head);
		q->head = next;
	}
	q->size--;
}

// 获取队列头部元素 
QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(q->head);//队列不能为空
	return q->head->data;
}

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

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

// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q)
{
	assert(q);
	return q->size == 0;
}

// 销毁队列 
void QueueDestroy(Queue* q)
{
	assert(q);
	while (q->head)//释放所有节点
	{
		QNode* next = q->head->next;
		free(q->head);
		q->head = next;
	}
	q->head = q->tail = NULL;
	q->size = 0;
}

test.c        main函数测试文件

#include"Queue.h"

void test1()
{
	Queue Q;
	QueueInit(&Q);//创建队列并初始化
	if (QueueEmpty(&Q))
		printf("队列空\n");
	else
		printf("队列非空\n");

	QueuePush(&Q, 1);//队列插入四个数据
	QueuePush(&Q, 2);
	QueuePush(&Q, 3);
	QueuePush(&Q, 4);
	if (QueueEmpty(&Q))
		printf("队列空\n");
	else
		printf("队列非空\n");
	printf("%d\n", QueueBack(&Q));//取队尾数据

	while (Q.size)//队列不为空时,每次取队首数据,再出队列
	{
		printf("%d ", QueueFront(&Q));
		QueuePop(&Q);
	}
	printf("\n");
	if (QueueEmpty(&Q))
		printf("队列空\n");
	else
		printf("队列非空\n");
	QueueDestroy(&Q);//队列销毁
}

int main()
{
	test1();
	return 0;
}

测试结果

六、应用场景

单链表队列的应用场景非常广泛,几乎在所有需要按照特定顺序处理数据的情况下都可以看到它的身影。以下是一些具体的应用场景示例:

  1. 操作系统中的任务调度:在操作系统中,任务(如进程或线程)经常需要按照某种顺序(如优先级、到达时间等)被调度执行。队列可以很好地满足这种需求,确保任务按照预定的顺序被处理。使用单链表实现的队列能够动态地添加和删除任务,非常适合这种场景。

  2. 数据包缓冲处理:在网络通信中,数据包经常需要在一个缓冲区中等待处理。队列结构能够确保数据包按照接收的顺序被处理,避免了乱序问题。单链表队列可以方便地添加新接收的数据包到队尾,并从队头取出数据包进行处理。

  3. 打印机任务队列:在打印多个文档时,打印机会按照接收文档的顺序进行打印。使用队列可以确保文档按照正确的顺序被处理。单链表队列可以动态地添加新的打印任务,并从队头取出任务进行打印。

  4. 事件驱动编程:在事件驱动编程模型中,事件(如用户输入、定时器事件等)会被放入一个事件队列中等待处理。使用队列可以确保事件按照发生的顺序被处理,避免了并发事件导致的混乱。单链表队列可以方便地添加新事件到队尾,并从队头取出事件进行处理。

  5. 游戏开发:在游戏中,经常需要处理大量的游戏对象(如玩家、怪物、子弹等)。使用队列可以确保这些对象按照特定的顺序(如创建顺序、优先级等)被更新或渲染。单链表队列可以动态地添加和删除游戏对象,提高游戏的性能和响应速度。

七、总结

通过本文的介绍,我们了解了如何使用单链表来实现队列,并探讨了其在实际应用中的重要性和应用场景。单链表队列具有动态分配内存、无需预先定义大小等优点,能够方便地添加和删除元素,满足各种按照顺序处理数据的需求。无论是在操作系统、网络通信、打印机任务处理、事件驱动编程还是游戏开发中,我们都可以看到单链表队列的身影。因此,掌握单链表队列的实现原理和应用方法对于程序员来说是非常有必要的。希望本文能够帮助读者更好地理解单链表队列的概念和应用,并在实际项目中灵活运用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/695070.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深圳中赢娱乐控股集团至江西省宜春市袁州区访问交流

2024年6月7日&#xff0c;深圳中赢娱乐控股集团受邀来到江西省宜春市袁州区就“短剧文旅”项目展开深度座谈&#xff0c;并与飞剑潭乡达成合作意向。 下午2:30&#xff0c;深圳中赢控股集团董事李平进带团队一行12人&#xff0c;访问宜春市袁州区&#xff0c;宜春市副市长谢萍、…

《C++ Primer Plus》第十三章复习题和编程练习

目录 一、复习题**二、编程练习 一、复习题** 1. 派生类从基类那里继承了什么&#xff1f; 答&#xff1a;在类的继承和派生中&#xff0c;C中的派生类能够继承基类的所有数据成员和大部分成员函数。但是基类中不同访问控制权限的成员在派生中的访问权限也不相同。公有成员直…

PGL图学习之图游走类metapath2vec模型[系列五]

本项目链接&#xff1a;https://aistudio.baidu.com/aistudio/projectdetail/5009827?contributionType1 有疑问查看原项目 相关项目参考&#xff1a; 关于图计算&图学习的基础知识概览&#xff1a;前置知识点学习&#xff08;PGL&#xff09;系列一 https://aistudio.…

企业网页制作

随着互联网的普及&#xff0c;企业网站已成为企业展示自己形象、吸引潜在客户、开拓新市场的重要方式。而企业网页制作则是构建企业网站的基础工作&#xff0c;它的质量和效率对于企业网站的成败至关重要。 首先&#xff0c;企业网页制作需要根据企业的特点和需求进行规划。在网…

MySQL的PrepareStatement真的是预编译语句么?

PrepareStatement真的是预编译语句么&#xff1f; ChatGPT对PrepareStatement的定义是&#xff1a; PrepareStatement 是 Java 数据库连接&#xff08;JDBC&#xff09;API 中用于执行预编译 SQL 语句的接口。通过使用 PreparedStatement&#xff0c;可以预编译 SQL 语句&…

Python代码大使用Paramiko轻松判断文件类型,提取上级目录

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; 一、Paramiko简介 Paramiko是一个用于SSHv2协议的Python实现&#xff0c;提供了客户端和服务器功能。它可以用于远程连接和管理服务器&#xff0c;执行命令、上传下载文件等。本文将介绍如何使用Paramiko判断文件类…

前端 Vue 操作文件方法(导出下载、图片压缩、文件上传和转换)

一、前言 本文对前端 Vue 项目开发过程中&#xff0c;经常遇到要对文件做一些相关操作&#xff0c;比如&#xff1a;文件导出下载、文件上传、图片压缩、文件转换等一些处理方法进行归纳整理&#xff0c;方便后续查阅和复用。 二、具体内容 1、后端的文件导出接口&#xff0c;…

C++程序设计:对数据文件的操作与文件流

姚老师小课堂开课啦&#xff01; 一、文件的分类&#xff1a; 1.ASCII码文件&#xff1a; ASCII文件使用方便&#xff0c;比较直观&#xff0c;便于阅读&#xff0c;便于对字符进行输入输出&#xff0c;但一般占用存储空间较多&#xff0c;而且需要花费转换时间&#xff08;二…

逆市创新高!水电“双雄“是怎样炼成的? 博通,赢麻了!

高分红夏季用电高峰AI的尽头是电力 6月7日&#xff0c;长江电力&#xff08;600900&#xff09;、华能水电&#xff08;600025&#xff09;股价双双上涨。截至收盘&#xff0c;长江电力股价上涨1%&#xff0c;收于28.31元/股&#xff1b;华能水电股价上涨1.52%&#xff0c;收于…

sqli-labs 靶场 less-8、9、10 第八关到第十关详解:布尔注入,时间注入

SQLi-Labs是一个用于学习和练习SQL注入漏洞的开源应用程序。通过它&#xff0c;我们可以学习如何识别和利用不同类型的SQL注入漏洞&#xff0c;并了解如何修复和防范这些漏洞。Less 8 SQLI DUMB SERIES-8判断注入点 当输入id为1时正常显示&#xff1a; 加上单引号就报错了 …

Kafka 架构

1 整体架构 1.1 Zookeeper Zookeeper 是一个分布式协调服务&#xff0c;用于管理 Kafka 的元数据。它负责维护 Kafka 集群的配置信息、Broker 列表和分区的 Leader 信息。 Zookeeper 确保了 Kafka 集群的高可用性和可靠性。 但 Zookeeper 已经成为 Kafka 性能瓶颈&#xff0c;…

什么是 AOF 重写?AOF 重写机制的流程是什么?

引言&#xff1a;在Redis中&#xff0c;持久化是确保数据持久性和可恢复性的重要机制之一。除了常见的RDB&#xff08;Redis Database&#xff09;持久化方式外&#xff0c;AOF&#xff08;Append Only File&#xff09;也是一种常用的持久化方式。AOF持久化通过记录Redis服务器…

基于xml的Spring应用(理解spring注入)

目录 问题&#xff1a; 传统Javaweb开发的困惑? 问题&#xff1a; IOC、DI和AOP的思想提出 问题&#xff1a; Spring框架的诞生 1. BeanFactory快速入门 2. ApplicationContext快速入门 3. BeanFactory和ApplicationContext的关系 基于xml的Spring应用 1. SpringBean的…

Mamba v2诞生:3 SMA与Mamba-2

大模型技术论文不断&#xff0c;每个月总会新增上千篇。本专栏精选论文重点解读&#xff0c;主题还是围绕着行业实践和工程量产。若在某个环节出现卡点&#xff0c;可以回到大模型必备腔调或者LLM背后的基础模型新阅读。而最新科技&#xff08;Mamba,xLSTM,KAN&#xff09;则提…

312. 戳气球 Hard

有 n 个气球&#xff0c;编号为0 到 n - 1&#xff0c;每个气球上都标有一个数字&#xff0c;这些数字存在数组 nums 中。 现在要求你戳破所有的气球。戳破第 i 个气球&#xff0c;你可以获得 nums[i - 1] * nums[i] * nums[i 1] 枚硬币。 这里的 i - 1 和 i 1 代表和 i 相邻…

JDK17 | Windows环境配置

众所周知&#xff0c; Jdk8做了很大的提升&#xff0c;网上的访谈&#xff0c;问到当下程序员要不要升级JDK版本的时候&#xff0c;得到异口同声的答案&#xff0c;不需要。这么多年过去了&#xff0c;数据是不会骗人的&#xff0c;现在Star最多的是JDK17&#xff0c;今天&…

STM32中ADC在cubemx基础配置界面介绍

ADCx的引脚,对应的不同I/O口&#xff0c;可以复用。 Temperature :温度传感器通道。 Vrefint :内部参照电压。 Conversion Trigger: 转换触发器。 IN0 至 IN15,是1ADC1的16个外部通道。本示例中输出连接的是ADC2的IN5通道&#xff0c;所以只勾选IN5.Temperature Sensor Cha…

搭建自己的组件库<2>dialog 组件

目录 设置title 插槽显示 控制宽高 关闭对话框 transition实现动画 引入深度选择器 同样创建组件dialogue.vue后全局注册 dialogue模版&#xff1a; <template><!-- 对话框的遮罩 --><div class"miao-dialog_wrapper"><!-- 真的对话框 …

python如何输入回车

Python默认遇到回车的时候&#xff0c;输入结束。所以我们需要更改这个提示符&#xff0c;在遇到空行的时候&#xff0c;输入才结束。 raw_input就是从标注输入读取输入&#xff0c;输入的是什么就是什么。 文档解释&#xff1a; The function then reads a line from input,…

debian系统apt 国内安装源

debian系统apt 国内安装源&#xff1a; 国内阿里镜像源&#xff1a; deb http://mirrors.aliyun.com/debian stable main non-free contrib deb-src http://mirrors.aliyun.com/debian stable main non-free contrib 打开源文件位置&#xff1a;/etc/apt/sources.list,原来的内…