当前位置: 首页 > news >正文

【面向小白】你见过这样讲解队列的吗?(阅此文可学会用纯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 队列的大小(有效元素的数目&#xff…...

[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 🚀数据结构专栏&#xff…...

【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15

缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)

引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作

一、上下文切换 即使单核CPU也可以进行多线程执行代码,CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短,所以CPU会不断地切换线程执行,从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”

2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...

docker 部署发现spring.profiles.active 问题

报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...

10-Oracle 23 ai Vector Search 概述和参数

一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...

论文笔记——相干体技术在裂缝预测中的应用研究

目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...

手机平板能效生态设计指令EU 2023/1670标准解读

手机平板能效生态设计指令EU 2023/1670标准解读 以下是针对欧盟《手机和平板电脑生态设计法规》(EU) 2023/1670 的核心解读,综合法规核心要求、最新修正及企业合规要点: 一、法规背景与目标 生效与强制时间 发布于2023年8月31日(OJ公报&…...

【汇编逆向系列】六、函数调用包含多个参数之多个整型-参数压栈顺序,rcx,rdx,r8,r9寄存器

从本章节开始,进入到函数有多个参数的情况,前面几个章节中介绍了整型和浮点型使用了不同的寄存器在进行函数传参,ECX是整型的第一个参数的寄存器,那么多个参数的情况下函数如何传参,下面展开介绍参数为整型时候的几种情…...