【面向小白】你见过这样讲解队列的吗?(阅此文可学会用纯C手撕一个队列)
目录
0.前言
1.什么是队列
2.选择什么结构实现队列
3.用C语言实现队列
3.1用什么可以封装代表一个队列
3.2队列接口的设计
3.3 队列的初始化
3.4 队列的销毁
3.5* 队列的状态分析
3.6 队列的插入
3.7 队列的删除
3.8 队列的大小(有效元素的数目)
3.9判断队列是否为空
3.10 获取队头数据
3.11 获取队尾数据
4.测试实现的队列
0.前言
4队列的实现 · onlookerzy123456qwq/data_structure_practice_primer - 码云 - 开源中国 (gitee.com)
https://gitee.com/onlookerzy123456qwq/data_structure_practice_primer/tree/master/4%E9%98%9F%E5%88%97%E7%9A%84%E5%AE%9E%E7%8E%B0本文所有代码都已上传至gitee,可以自取。
1.什么是队列
我们想象一个管道,在这个管道运行的时候,比如我们现在要输送石油从新疆到上海,那么这个管道当中,石油是从新疆端 入的,石油是从上海端 出的,并且石油只允许从新疆输入,石油只允许从上海端输出。同时在管道里的石油,肯定是先输入的石油更快到达上海,后面输入的石油是在先输入的石油到达之后,才到达上海(因为先输入的石油是堵在管道前面的,必须先进的先出之后,后进的才能出)。
这个管道就是一个队列。先后输入的石油就是队列里面的元素。
栈是只允许在一端进行插入/删除,而我们的队列与之相反,队列是只允许在一端进行插入,只能在相反的另一端进行删除。
同时队列也是只能先进先出的,后进后出的,先入队列的元素肯定是堵在后入队列的元素的前面,必须先入队列的数据先出队列之后,相对后入的队列才能出队列。
负责插入(入队列)的一端称为队尾,负责删除(出队列)的一端称之为队头。
2.选择什么结构实现队列
实现队列我们可供选择的有两种结构,一种是顺序表结构,一种是链式结构,在这个效率至上的时代,我们肯定是选择链式结构实现队列!因为队列是只允许在一端插入,另一端删除的,所以我们就需要 头插配尾删(此时顺序表/链表的头部是队尾,尾部是队头) 或者是 头删配尾插(此时此时顺序表/链表的头部是队头,尾部是队尾),而我们的顺序表天然在头部进行插入删除的效率是极低的,效率为O(N),因为需要挪动数据。所以链表结构便成为了不二选择。
我们以单链表的头head 作为队列的队头(出数据),以单链表的尾tail 作为队列的队尾(入数据):
单链表头删的效率是O(1),但是单链表尾插由于需要找尾,效率会降为O(N),这是这并没有太大的关系,因为我们在用链式结构实现队列的时候,可以一直记录tail尾的位置,即我们在封装Queue的时候,除了记录头节点head的位置,也记录住尾节点tail的位置,每次删除/插入操作的时候,适当更新tail,就不用找尾,此时我们对tail尾部的插入的效率就也是O(1)了。
3.用C语言实现队列
3.1用什么可以封装代表一个队列
我们选择的是一个链式结构作为基础实现队列,所以我们要保存一个单链表,如何代表一个单链表,其实我们只需要记录一个单链表的头head即可。同时为了方便我们进行尾插,我们还需要维护一个单链表的tail指针。
所以队列通过一个链表节点head和tail指针就可代表了。
所以我们定义一个存储队列数据的节点struct Node类,然后对封装的Queue类当中,封装两个节点指针head,tail即可。
//范式类型
typedef int QDataType;//节点
typedef struct QueueNode
{QDataType _data;struct QueueNode* _next;
}QueueNode;//队列
typedef struct Queue
{QueueNode* _head;//队头QueueNode* _tail;//队尾
}Queue;
3.2队列接口的设计
我们要修改的是一个队列Queue q对象,我们就不能做对Queue q的直接传值传参,因为这样的话,就是在调用的函数栈帧当中,拷贝一个相同的Queue tmp_q,并不是这个我们的q。
所以我们应该传入的是main函数当中Queue q对象的指针,即Queue* pq,你传入的是q对象的指针,这样pq指向的就是我main函数中的q实体了,在Func函数当中指针找到的也是main函数当中的q实体。
所以接口我们这样设计:
//队列相关接口
/*传入一级指针即可:传入(指向[Queue两个指针实体]的指针,可以修改Queue *head *tail实体)*/
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
bool QueueEmpty(Queue* pq);
int QueueSize(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
3.3 队列的初始化
我们定义出来一个队列类的对象,那这个对象里面的_head和_tail一定是随机值野指针,如果不对之进行初始化,那后续我们对这个队列进行插入删除等操作的时候,灾难性的野指针问题就会暴露出来(非法访问)。
所以每次定义一个队列,我们都要对这个队列对象进行初始化:
void QueueInit(Queue* pq)
{//须传入队列实体assert(pq);pq->_head = NULL;pq->_tail = NULL;
}
3.4 队列的销毁
每次使用完队列,我们不能就不管了,因为堆区申请的空间,需要我们主动释放,在这个队列的实现当中,我们是在堆区里申请了一个一个的节点组成了一个链表,如果不释放的话,那么就会造成内存泄漏,这些堆区空间就会一直被“霸占”着,无法被再使用。
所以每次使用完一个队列,我们都要对之释放清理资源:
void QueueDestroy(Queue* pq)
{//须传入队列实体assert(pq);//依次释放所有的节点QueueNode* cur = pq->_head;while(cur){QueueNode* next = cur->_next;free(cur);cur = next;}//置空,防止野指针pq->_head = NULL;pq->_tail = NULL;
}
3.5* 队列的状态分析
我们分析一下队列在各个状态下,_head _tail成员的实际情况:
队列空的时候,head和tail都是NULL。
在通常情况下,队列_head指向队头的数据节点 ,负责删除更新(头删),_tail指向队尾的数据节点,负责插入更新(尾插)。
再紧接着分析一下队列的插入删除,对_head_tail成员的影响:
对队列插入就是在_tail的后面进行链接新创建的节点,并记录更新_tail为新节点。对队列的删除就是对_head所在位置的节点释放删除,并记录更新_head为原来_head的下一个节点的位置。
但是这样真的就行了吗?入队列,即尾插,真的只更新_tail就行了吗?出队列,即头删,真的只更新_head就行了吗?当然不行!!!
万一现在是空队列,_head和_tail都是NULL空,那你入队列,插入一个数据,此时_head和_tail都需要更新为新节点,不能只顾_tail。万一现在队列只有一个节点,那你删除之后,此时_head和_tail也都想需更新为NULL,不能只顾_head。
3.6 队列的插入
大致思路就是,创建新节点,然后链接到tail后面,并更新tail。
检查队列是合法的(用户传入的是一个有效队列指针);提前判断一开始队列就是空的特殊情况,此时除了_tail需要更新为新节点,_head也需要更新。
void QueuePush(Queue* pq,QDataType x)
{//须传入队列实体assert(pq);//创建新节点QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));newnode->_next = NULL;newnode->_data = x;/*插入分两种情况情况一:直接在tail后面链接新节点然后更新tail即可情况二:队列为空,head==tail==NULL,此时需要更新head+tail为新节点指针*/if (pq->_head == NULL)//QueueEmpty(pq){pq->_head = newnode;pq->_tail = newnode;}else {pq->_tail->_next = newnode;pq->_tail = newnode;}
}
3.7 队列的删除
大致思路就是,把_head指向的队头的节点进行释放删除,然后更新_head为下一个节点的位置。
检查队列是合法的(用户传入的是一个有效队列指针);检查此时队列不为空,为空,即没有有效数据不能进行删除,需要报错;提前判断一开始队列就是只有一个节点的特殊情况,此时除了_tail需要更新为NULL,_head也需要更新,且更新为NULL。
void QueuePop(Queue* pq)
{//传入有效队列assert(pq);//有有效数据,即队列非空才可以删assert(!QueueEmpty(pq));//pq->_head != NULL/*分两种情况情况一:直接删除head节点,然后更新head到next第二个节点的指针情况二:现在只有一个节点,删除该节点后head变成next空节点NULL,此时tail也需更新为NULL,因为现在tail还指向着刚刚删除的节点的空间,出现野指针问题*/QueueNode* next = pq->_head->_next;free(pq->_head);pq->_head = next;if (pq->_head == NULL)//删除到空{pq->_tail = NULL;}
}
3.8 队列的大小(有效元素的数目)
获取当前队列里面一共有多少个有效数据,其实就是看从_head到_tail,这两个节点之间有多少个有效节点数据。思路就是依次从_head往后直至遍历到NULL,统计数量。
int QueueSize(Queue* pq)
{//须传入队列实体assert(pq);//链式结构通常不存节点数目//所以通常进行遍历统计int size = 0;QueueNode* cur = pq->_head;while (cur){cur = cur->_next;++size;}return size;
}
3.9判断队列是否为空
队列为空的时候_head为NULL。
bool QueueEmpty(Queue* pq)
{ //须传入队列实体assert(pq);//队头是空即可return pq->_head == NULL;
}
3.10 获取队头数据
获取队头的数据,即下一个要出队列的队头QueueFront,但是需要判断队列不为空,如果队列为空即没有有效数据的话,那就是需要报错的。
QDataType QueueFront(Queue* pq)
{//传入有效队列实体assert(pq);//必须有数据(队列非空)才能取assert(!QueueEmpty(pq));//Front就是队头head的元素数据return pq->_head->_data;
}
3.11 获取队尾数据
获取队尾的数据,即最近一次插入的队列的队尾QueueBack,但是需要判断队列不为空,如果队列为空即没有有效数据的话,那就是需要报错的。
QDataType QueueBack(Queue* pq)
{//传入有效队列实体assert(pq);//必须有数据(队列非空)才能取assert(!QueueEmpty(pq));//Back就是队尾tail的元素数据return pq->_tail->_data;
}
4.测试实现的队列
任何实现都离不开测试,这里笔者建议大家,不要写一堆之后再测试,应该最好是写一点测一点,写一个接口测试一下,这样可以帮助我们快速的定位问题bug的所在处。这并不是在浪费时间,而是在节省时间。
void QueueTest1()
{Queue q;QueueInit(&q);printf("Queue Size:%d\n", QueueSize(&q));printf("Queue Empty:%d\n", QueueEmpty(&q));QueueDestroy(&q);
}
void QueueTest2()
{Queue q;QueueInit(&q);QueuePush(&q,5);QueuePush(&q, 9);QueuePush(&q, 7);printf("Queue Size:%d\n", QueueSize(&q));printf("Queue Empty:%d\n", QueueEmpty(&q));QueueDestroy(&q);
}
void QueueTest3()
{Queue q;QueueInit(&q);QueuePush(&q, 1);QueuePush(&q, 0);QueuePush(&q, 0);QueuePush(&q, 8);QueuePush(&q, 6);printf("Queue Size:%d\n", QueueSize(&q));printf("Queue Empty:%d\n", QueueEmpty(&q));QueuePop(&q);printf("Queue Size:%d\n", QueueSize(&q));QueueDestroy(&q);
}
void QueueTest4()
{Queue q;QueueInit(&q);QueuePush(&q, 1);printf("Queue Back:%d ", QueueBack(&q));QueuePush(&q, 0);printf("Queue Back:%d ", QueueBack(&q));QueuePush(&q, 0);printf("Queue Back:%d ", QueueBack(&q));QueuePush(&q, 8);printf("Queue Back:%d ", QueueBack(&q));QueuePush(&q, 6);printf("Queue Back:%d ", QueueBack(&q));printf("\n");printf("Queue Size:%d\n", QueueSize(&q));printf("Queue Empty:%d\n", QueueEmpty(&q));//测试具体的遍历栈的过程//数据是先进先出while (!QueueEmpty(&q)){printf("Queue Front:%d Pop\n", QueueFront(&q));QueuePop(&q);}
}
int main()
{//QueueTest1();//QueueTest2();//QueueTest3();QueueTest4();return 0;
}
相关文章:

【面向小白】你见过这样讲解队列的吗?(阅此文可学会用纯C手撕一个队列)
目录 0.前言 1.什么是队列 2.选择什么结构实现队列 3.用C语言实现队列 3.1用什么可以封装代表一个队列 3.2队列接口的设计 3.3 队列的初始化 3.4 队列的销毁 3.5* 队列的状态分析 3.6 队列的插入 3.7 队列的删除 3.8 队列的大小(有效元素的数目ÿ…...

[element plus] 对话框组件再封装使用 - vue
学习关键语句: 饿了么组件dialog组件使用 dialog组件二次封装 vue3中封住的组件使用update触发更新 vue3中封装组件使用v-model:属性值来传值 写在前面 这是我遇到的一个页面需求 , 其中一个对话框的内容是很常用的 , 所以我将它封装出来才写的一篇文章 现在给出如下需求: 封…...

Markdown基本语法简介
前言:当你在git平台创建一个仓库时,平台会自动创建一个README.md文件,并将它的内容展现在web端页面,方面其他读者查阅。README.md实则是一个适用Markdown语法的文本文件,从他的后缀md即可看出它是Markdown的缩写。在gi…...

分布式服务的接口幂等性如何设计
1.1 概述 所谓幂等: 多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。 基于RESTful API的角度对部分常见类型请求的幂等性特点进行分析 举个例子: 假如你有个某多多 有个服务 服务提供一个接口,结果这个服务部署在…...

视频流截取保存到本地路径(打包jar包CMD运行)
需求:现在有一批https的监控视频流URL,需要对视频流进行每三秒截屏一次,并保存到本地路径,png格式,以当前时间命名。代码:import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.Fra…...

mysql索引失效的几种情况
失效的几种情况 1、select * from xxx 2、索引列上有计算 3、索引列上有函数 4、like左边包含‘%’ 5、使用or关键字 6、not in和not exists 7、order by 8、不满足最左匹配原则 给code、age和name这3个字段建好联合索引:idx_code_age_name。 该索引字段的顺…...

Windows下载安装Redis的详细步骤
目录 一、概述 1.redis的版本维护介绍 2.msi安装包和压缩包的优点和缺点 二、操作步骤 三、测试是否安装成功(查看版本) 四、获取资源 一、概述 1.redis的版本维护介绍 Redis的官网只提供Linux系统的下载。但是微软的技术团队长期开发和维护着这…...

【蓝桥杯每日一题】差分算法
🍎 博客主页:🌙披星戴月的贾维斯 🍎 欢迎关注:👍点赞🍃收藏🔥留言 🍇系列专栏:🌙 蓝桥杯 🌙我与杀戮之中绽放,亦如黎明的花…...

MyBatis Plus 数据库字段加密处理
目录1.场景介绍2.Maven依赖2.AESUtil.java 加解密工具类3.字段处理类4.修改 MyBatis Plus 查询4.1 修改表对应实体类4.2 修改加密字段对应属性4.3 修改 xml 使用 ResultMap4.4 修改 xml 中 el 表达式5.测试结果6.MyBatis Plus 缺陷补充:测试实例1 查询测试1.1 查询信…...

openpose在win下环境配置
1.下载OpenPose库 以下二选一进行下载源码 (1)git进行下载 打开GitHub Desktop或者Powershell git clone https://github.com/CMU-Perceptual-Computing-Lab/openpose cd openpose/ git submodule update --init --recursive --remote(2)在github上手动下载 由于下载环境问…...

【剑指offer-C++】JZ16:数值的整数次方
【剑指offer】JZ16:数值的整数次方题目描述解题思路题目描述 描述:实现函数 double Power(double base, int exponent),求base的exponent次方。 注意: 1.保证base和exponent不同时为0。 2.不得使用库函数,同时不需要…...

了解Axios及其运用方式
Axios简介 axios框架全称(ajax – I/O – system): 基于promise用于浏览器和node.js的http客户端,因此可以使用Promise API 一、axios是干啥的 说到axios我们就不得不说下Ajax。在旧浏览器页面在向服务器请求数据时,…...

【LeetCode】剑指 Offer(7)
目录 写在前面: 题目剑指 Offer 17. 打印从1到最大的n位数 - 力扣(Leetcode) 题目的接口: 解题思路: 代码: 过啦!!! 题目:剑指 Offer 18. 删除链表的节…...

Python:try except 异常处理整理
目录 一、try except异常处理的语句格式 二、获取相关异常信息 (1)sys.exec_info() 三、traceback模块的常用方式 (1)traceback.print_tb(tb, limitNone, fileNone) 打印指定堆栈异常信息 (2)tracebac…...

Redis Lua脚本的详细介绍以及使用入门
Redis Lua脚本的详细介绍以及使用入门。 文章目录Redis Lua脚本的引入开源软件的可扩展性Redis的扩展性脚本Redis Lua脚本的基本使用通过EVAL命令执行Lua脚本通过脚本与Redis交互Java中调用Redis Lua脚本Java调用Lua脚本的方式Redis Lua脚本的使用建议脚本缓存脚本缓存稳定性脚…...

synchronized和ReentrantLock有什么区别呢?
第15讲 | synchronized和ReentrantLock有什么区别呢? 从今天开始,我们将进入 Java 并发学习阶段。软件并发已经成为现代软件开发的基础能力,而 Java 精心设计的高效并发机制,正是构建大规模应用的基础之一,所以考察并发…...

SVHN数据集下载及使用方法
街景门牌号数据集(SVHN),这是一个现实世界数据集,用于开发目标检测算法。它需要最少的数据预处理过程。它与 MNIST 数据集有些类似,但是有着更多的标注数据(超过 600,000 张图像)。这些数据是从…...

产业安全公开课:2023年DDoS攻击趋势研判与企业防护新思路
2023年,全球数字化正在加速发展,网络安全是数字化发展的重要保障。与此同时,网络威胁日益加剧。其中,DDoS攻击作为网络安全的主要威胁之一,呈现出连年增长的态势,给企业业务稳定带来巨大挑战。2月21日&…...

Docker 容器命令 和安装各种镜像环境
CentOS安装Docker 1.1.卸载(可选) 如果之前安装过旧版本的Docker,可以使用下面命令卸载: yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotat…...

【数据结构】顺序表的深度剖析
🌇个人主页:平凡的小苏 📚学习格言:别人可以拷贝我的模式,但不能拷贝我不断往前的激情 🛸C语言专栏:https://blog.csdn.net/vhhhbb/category_12174730.html 🚀数据结构专栏ÿ…...

当面试官问“你的SQL能力怎么样”时,怎么回答才不会掉进应聘陷阱?
在某平台看到一个比较实际的问题,在这里分享给职场新人。 SQL已经是职场最常用的一种编程语言,所以应聘技术或非技术岗位,都可能会被问道一个问题:你的SQL能力怎么样? 对于职场新人来说(SQL高手可以无视下…...

AI作画—中国画之山水画
山水画,简称“山水”,中国画的一种,描写山川自然景色为主体的绘画。山水画在我国绘画史中占有重要的地位。 山水画形成于魏晋南北朝时期,但尚未从人物画中完全分离。隋唐时始终独立,五代、北宋时趋于成熟,…...

Java:Java与Python — 编码大战
Java和Python是目前市场上最热门的两种编程语言,因为它们具有通用性、高效性和自动化能力。两种语言都有各自的优点和缺点,但主要区别在于Java 是静态类型的,Python是动态类型的。它们有相似之处,因为它们都采用了“一切都是对象”…...

山东专精特新各地市扶持政策
青岛市奖励政策:新认定为市隐形、省“专精特新”及省瞪羚、角兽的我市企业,分别给予50万元、30万元、50万元、300万元的一次性奖励。奖励金额:省级30万济南市奖励政策:对被认定的国家专精特新 “小巨人”企业一次性给予200万元奖励…...

持续事务管理过程中的事件驱动
比较官方的定义:事件驱动是指在持续事务管理过程中,进行决策的一种策略,即跟随当前时间点上出现的事件,调动可用资源,执行相关任务,使不断出现的问题得以解决,防止事务堆积。在计算机编程、公共…...

【手把手一起学习】(三) Altium Designer 20 原理图库添加元件
1 添加元件 元件符号是元件在原理图上的表现形式,主要由边框、管脚、名称等组成,原理图库中的元件管脚(顺序,间距等)与电子元件实物的引脚严格对应,绘制原理图库时,一定参考元件规格书和芯片数据手册中的说明…...

设计模式-行为型模式:观察者模式
目录 1、简介 2、组成部分 3、优缺点 4、使用场景 5、代码实现 1、简介 观察者模式是一种软件设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听一个主题对象,当主题对象发生变化时,所有的观察者对象都会得到…...

Springboot 为了偷懒,我封装了一个自适配的数据单位转换工具类
前言 平时做一些统计数据,经常从数据库或者是从接口获取出来的数据,单位是跟业务需求不一致的。 比如, 我们拿出来的 分, 实际上要是元 又比如,我们拿到的数据需要 乘以100 返回给前端做 百分比展示 又比如ÿ…...

正则表达式
当我们需要对字符串进行判断的时候,使用正则表达式能大大提高编程效率。比如,当我们需要找出所有“像邮箱”的字符串(包含"" "." ".com",且顺序一致),我们需要一个某种模式的…...

java进阶Map 集合
通过之前的学习我们知道Map是一个双列集合,就是以键值对的形式进行数据存储 java进阶—集合 Map 下面有 三个子接口,HashMap , HashTable 以及 TreeMap 提醒一点:Map不是Collection下的集合,Collection是单列集合&am…...