【数据结构初阶】队列接口实现及用队列实现栈超详解

文章目录

  • 1. 概念
    • 1. 1 队列底层结构选型
    • 1. 2 队列定义
  • 2. 接口实现
    • 2. 1 初始化
    • 2. 2 判空
    • 2. 3 入队列
    • 2. 4 出队列
    • 2. 5 队尾元素和队头元素和队列元素个数
    • 2. 6 销毁
    • 2. 7 接口的意义
  • 3. 经典OJ题
    • 3. 1 用队列实现栈
      • 3. 1. 1 栈的定义
      • 3. 1. 2 栈的初始化
      • 3. 1. 3 入栈
      • 3. 1. 4 出栈
      • 3. 1. 5 取栈顶元素
      • 3. 1. 6 判空
      • 3. 1. 7 销毁


1. 概念

概念:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FlFO(First In First Out)的特点。

入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
FIFO

1. 1 队列底层结构选型

和栈一样,队列也可以数组和链表的结构实现,但是使用链表的结构实现更优一些,因为队列需要在队头出数据,如果使用数组的结构,出队列就是在数组头上出数据,效率会比较低。

1. 2 队列定义

既然使用链表实现队列,那么栈就应该有两个自定义结构,一个是节点结构体,一个是整个队列的结构体

//队列节点
typedef int QDataType;
typedef struct QListNode
{struct QListNode* next;QDataType data;
}QNode;// 队列的结构 
typedef struct Queue
{QNode* head;QNode* tail;int size;
}Queue;

队列节点和普通的单链表节点是一样的,而队列的结构中,需要保存队列的头和尾,方便进行出队列和入队列,除此之外还要保存队列中元素的个数。

2. 接口实现

2. 1 初始化

void QueueInit(Queue* q);

我们依然采用先在main函数中创建队列,再在初始化函数中进行初始化处理的方式,原因已经在栈的实现讲解过了。

对一个Queue类型的结构体进行初始化,就是将所有的元素置为NULL0就可以了。

void QueueInit(Queue* q)
{assert(q);q->head = NULL;q->tail = NULL;q->size = 0;
}

2. 2 判空

判断队列是不是为空,接下来的几个函数会用到这个接口。
只需要判断头节点或者尾节点是否为空就可以了。

int QueueEmpty(Queue* q)
{assert(q);return q->head == NULL;
}

2. 3 入队列

入队列是在队尾入队列,有以下几个步骤:

  1. 申请新节点。
  2. 将其接到原来的尾节点的后面,并将尾节点指向新节点。
  3. size++
void QueuePush(Queue* q, QDataType data)
{assert(q);//1. 申请新节点QNode* newnode = (QNode*)malloc(sizeof(QNode));if (!newnode){perror("malloc");exit(1);}newnode->data = data;//next记得置空newnode->next = NULL;//2. 插入节点if (!QueueEmpty(q)){q->tail->next = newnode;q->tail = q->tail->next;}//如果队列为空,要让头结点和尾节点都指向新节点elseq->tail = q->head = newnode;//3. size++q->size++;
}

2. 4 出队列

void QueuePop(Queue* q);

从队头出队列,有以下几个步骤:

  1. 对链表进行前删,并修改headtail的指向
  2. size--
void QueuePop(Queue* q)
{assert(q);//出队列时要判空,如果为空,就非法assert(!QueueEmpty(q));//1. 链表前删//将头节点的下一个节点保存起来,作为新的头节点QNode* next = q->head->next;free(q->head);//如果原本队列中只有一个元素,那么出队列后tail就指向的是一个野指针了,要置为空if (q->size == 1)q->tail = NULL;q->head = next;//2. size--q->size--;
}

2. 5 队尾元素和队头元素和队列元素个数

// 获取队列头部元素 
QDataType QueueFront(Queue* q);
// 获取队列队尾元素 
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数 
int QueueSize(Queue* q);

直接返回就可以了,不过要注意判空,否则会发生空指针的解引用。

QDataType QueueFront(Queue* q)
{assert(q);assert(!QueueEmpty(q));return q->head->data;
}QDataType QueueBack(Queue* q)
{assert(q);assert(!QueueEmpty(q));return q->tail->data;
}int QueueSize(Queue* q)
{assert(q);return q->size;
}

2. 6 销毁

void QueueDestroy(Queue* q);

将底层链表中所有节点free掉,再将Queue结构中所有成员置为NULL0就可以了。

void QueueDestroy(Queue* q)
{assert(q);//需要判空吗?//assert(!QueueEmpty(q));//释放链表节点QNode* pcur = q->head;while (pcur){QNode* next = pcur->next;free(pcur);pcur = next;q->size--;}//将Queue结构体中所有成员置为NULL或0q->head = q->tail = NULL;q->size = 0;
}

2. 7 接口的意义

在队列这个数据结构上,我们实现了8个接口,可是其中有些接口,比如判空,元素个数等几个接口内部除了断言以外就一两行代码,为什么还要费力去实现呢?

assert(!QueueEmpty(q));
assert(q->head!=NULL);

下面的写法难道不是更简洁吗?

事实上,这是因为每个人实现的队列中的成员名称可能不同,比如可能有个人的实现中,Queue是这么定义的:

typedef struct Queue
{QNode* _head;QNode* _tail;int _size;
}Queue;

那么上面的代码就会报错了。
这样一比较,使用原来的开发者制作的接口就比较安全了。

3. 经典OJ题

3. 1 用队列实现栈

题目链接

1

//题目给出代码:
typedef struct {} MyStack;MyStack* myStackCreate() {}void myStackPush(MyStack* obj, int x) {}int myStackPop(MyStack* obj) {}int myStackTop(MyStack* obj) {}bool myStackEmpty(MyStack* obj) {}void myStackFree(MyStack* obj) {}/*** Your MyStack struct will be instantiated and called as such:* MyStack* obj = myStackCreate();* myStackPush(obj, x);* int param_2 = myStackPop(obj);* int param_3 = myStackTop(obj);* bool param_4 = myStackEmpty(obj);* myStackFree(obj);
*/

这道题需要我们实现以上给出接口和结构体定义。

在开始写这道题之前,我们先想一下怎么用两个队列去实现栈
首先,保证所有元素都在同一个队列(先称其为q1,另一个为q2)中,插入时只需要直接向q1入队列就可以了。

那么怎么出栈呢?
我们把q1中的元素依次出队列并入队列至q2,这样元素的顺序不会变,直到q1中只剩下一个元素,把这个元素出队列而不入队列,不就实现了出栈了吗?

当然,在出栈之后,q1q2是否存储数据的情况就颠倒了,也就是说,q1q2哪个存储数据是不一定的,在实现时要注意这一点。

我们正式来写这道题,第一步是实现队列这个数据结构,因为C语言是没有库提供其实现的,这里做了一些简化:

typedef struct QueueNode{struct QueueNode* next;int data;
}QNode;typedef struct Queue{QNode *head;QNode *tail;int size;
}QU;
//判空
int IsEmpty(QU* qu)
{return qu->head == NULL;
}
//入队列
void PushToBack(QU* qu,int x)
{QNode* newnode = (QNode*)malloc(sizeof(QNode));newnode->data = x;newnode->next = NULL;if(IsEmpty(qu)){qu->head = qu->tail=newnode;}else{qu->tail->next = newnode;qu->tail = newnode;}qu->size++;
}
//出队列,在这道题中由于不允许使用其他接口,所以出队列函数要额外返回出队列的数据。
int PeekFromFront(QU* qu)
{QNode* newhead = qu->head->next;int ret = qu->head->data;free(qu->head);qu->head = newhead;if(!qu->head)qu->tail = NULL;qu->size--;return ret;
}
//初始化
void InitQU(QU* qu)
{qu->head = qu->tail=NULL;qu->size = 0;
}
//销毁,其实在OJ题中,一般不会出现内存溢出,所以可以不考虑内存溢出,但为了代码的严谨性,最好还是释放掉内存。
void Destroy(QU* qu)
{QNode* cur = qu->head;while(cur){QNode* next = cur->next;free(cur);cur = next;}qu->head = qu->tail = NULL;qu->size = 0;
}

在写OJ题时,不需要考虑malloc失败的情况,也不需要任何断言(当然加上也可以)。

以上是队列的实现,下面我们逐一来看栈的实现:

3. 1. 1 栈的定义

typedef struct {QU* q1;QU* q2;int size;
} MyStack;

两个队列指针,一个int变量存储数据的个数。

3. 1. 2 栈的初始化

MyStack* myStackCreate();

由于函数声明由题目给出,所以我们必须写成在函数内部动态申请内存然后返回的形式。
不仅申请MyStack这个结构体的内存,还要为其中的两个队列申请内存,因为MyStack结构体中只存在两个指向QU类型的指针变量而不是QU变量。

MyStack* myStackCreate() {MyStack* MS = (MyStack*)malloc(sizeof(MyStack));//为两个队列申请空间MS->q1 = (QU*)malloc(sizeof(QU));MS->q2 = (QU*)malloc(sizeof(QU));//分别对两个队列进行初始化InitQU(MS->q1);InitQU(MS->q2);MS->size = 0;return MS;
}

3. 1. 3 入栈

void myStackPush(MyStack* obj, int x);

上面已经说过了,只需要向有数据的队列入队列这个数据就可以了**,如果都没有数据,向任意队列入数据**就行。

void myStackPush(MyStack* obj, int x) {//找有数据的队列,都没有就用 q1QU* use = obj->q1;if (IsEmpty(use))use = obj->q2;//在找到的队列入队列这个数据PushToBack(use, x);obj->size++;
}

3. 1. 4 出栈

int myStackPop(MyStack* obj);

这个出栈函数需要返回出栈的数据

按照我们前面所说的,有几个步骤:

  1. 找到有数据的队列
  2. 将有数据的队列中的除了最后一个元素外全部出队列并入队列到另一个队列中
  3. 将原本有数据的队列的最后一个元素出队列并返回
int myStackPop(MyStack* obj) {// 1. OJ题一般不需要考虑两个队列都为空的情况QU* use = obj->q1;QU* other = obj->q2;if (IsEmpty(use)) {use = obj->q2;other = obj->q1;}// 2while (use->head->next) {PushToBack(other, PeekFromFront(use));}// 3return PeekFromFront(use);
}

3. 1. 5 取栈顶元素

int myStackTop(MyStack* obj);

其实步骤和出栈是几乎一样的,只是最后的那个元素在出队列之后还需要入队列到另一个队列中

int myStackTop(MyStack* obj) {QU* use = obj->q1;QU* other = obj->q2;if (IsEmpty(use)) {use = obj->q2;other = obj->q1;}while (use->head->next) {PushToBack(other, PeekFromFront(use));}//以上都和出栈一样//将最后一个元素的值保存起来方便返回int ret = PeekFromFront(use);//将其入队列到另一个队列中PushToBack(other, ret);return ret;
}

3. 1. 6 判空

如果两个队列都为空,那么这个栈就是空的。

bool myStackEmpty(MyStack* obj) 
{ return IsEmpty(obj->q1) && IsEmpty(obj->q2); 
}

3. 1. 7 销毁

将两个队列销毁,再将两个队列本身和栈本身都free掉就可以了。

void myStackFree(MyStack* obj) {Destroy(obj->q1);Destroy(obj->q2);//栈和队列都是动态开辟的,所以都需要释放free(obj->q1);free(obj->q2);free(obj);
}

这道题的Leetcode官方题解使用的是以数组为底层的队列去实现栈,但是由于本文是用的链表,所以还是用的链表,感兴趣可以研究一下怎么使用数组实现队列。

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会持续更新更多优质文章

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

微调大模型不再难:LoRA方法带你轻松节省99%的训练成本!

我们之前说大模型有四种玩家,其中前三种都是要做模型训练的。而大部分公司或个人,都是在第二种开源大模型的基础上来做训练。 而这种训练方式又分为两种。一种要么就是从头训练,要么就Fine-tuning接着开源模型来训练,在基座模型已…

【CPP】模板(后篇)

目录 13.1 非类型模板参数13.2 函数模板的特化13.3 类模板的特化13.4 模板的分离编译 这里是oldking呐呐,感谢阅读口牙!先赞后看,养成习惯! 个人主页:oldking呐呐 专栏主页:深入CPP语法口牙 13.1 非类型模板参数 顾名思义,非类型模板参数就是一个模板的参数,只不过不是类型,而…

基于深度学习的图像去雾研究进展

🌞欢迎莅临我的个人主页👈🏻这里是我专注于深度学习领域、用心分享知识精粹与智慧火花的独特角落!🍉 🌈如果大家喜欢文章,欢迎:关注🍷点赞👍🏻评论…

个人随想-gpt-o1大模型中推理链的一个落地实现

​首先祝大家中秋节快乐。 最近openai又推出了新的模型openai o1​还有它的mini版。官网的介绍,就是它的推理能力很强,比gpt-4o​有很大的提升。 最近也跟同行在聊这个o1,​看看落地方面有哪些可行性。在我们自己的实验上,把o1用…

JavaScript web API完结篇---多案例

BOM window对象 >包含docment Browser Object Model 定时器–延时函数 之前学的是间歇函数 让代码延迟执行 仅执行一次 setTimeout(回调函数,等待毫秒数) 消除 clearTimeout(timer) > 用于递归时需要进行去除 JS执行机制 单线程 > 一个任务结束&…

【C++笔记】类和对象的深入理解(三)

【C笔记】类和对象的深入理解(三) 🔥个人主页:大白的编程日记 🔥专栏:C笔记 文章目录 【C笔记】类和对象的深入理解(三)前言一.日期类的实现1.1声明和定义分离1.2日期类整数1.3日期类整数1.4日期类-整数1.5日期类-日期1.6复用对…

Linux中安装maven

Linux中安装maven 1.下载2.安装3.配置环境变量4.maven相关配置 1.下载 下载地址:https://maven.apache.org/download.cgi 2.安装 指定位置上传压缩包: 解压: tar -zxvf apache-maven-3.9.5-bin.tar.gz修改解压缩后的文件名: mv apac…

Netty笔记09-网络协议设计与解析

文章目录 前言一、协议设计1. 数据格式2. 消息长度3. 编码方式4. 错误处理5. 安全性 二、协议解析1. 消息分隔2. 粘包与半包处理3. 校验机制 三、为什么需要协议?四、redis 协议五、HTTP 协议六、自定义协议要素编解码器💡 什么时候可以加 Sharable 前言…

【论文阅读】Act3D: 3D Feature Field Transformers for Multi-Task Robotic Manipulation

Abstract 3d感知表示非常适合机器人操作,因为它们很容易编码遮挡并简化空间推理。许多操作任务在末端执行器姿态预测中需要较高的空间精度,这通常需要高分辨率的3d特征网格,这对于处理来说计算成本很高。因此,大多数操作policies…

基于密码的大模型安全治理的思考

文章目录 前言一、大模型发展现状1.1 大模型技术的发展历程1.2 大模型技术的产业发展二、大模型安全政策与标准现状2.1 国外大模型安全政策与标准2.2 我国大模型安全政策与标准前言 随着大模型技术的迅速发展和广泛应用,其安全性问题日益凸显。密码学作为网络空间安全的核心技…

Unity webgl跨域问题 unity使用nginx设置跨域 ,修改请求头

跨域 什么是跨域 跨域是指浏览器因安全策略限制,阻止一个域下的网页访问另一个域下的资源。 一些常见的跨域情况: 协议不同 从 http://example.com 请求 https://example.com。域名不同 从 http://example.com 请求 http://anotherdomain.com。端口不…

《锐捷AP 胖模式配置示例》

目录 WEB配置方式: 1. 登录 AP 管理界面 2. 配置无线服务 3. 配置射频参数 4. 配置 VLAN (如果需要) 5. 配置 IP 地址 6. 其他高级设置(根据需求) 命令行配置: 1. 进入特权模式 2. 进入全局配置模式 3. 配置管理 IP 地址 4. 创建无线 SSID 5. 配置 SSID 加密…

多线程下的共享变量访问数据竞争的问题

多线程下对共享变量的写存在数据竞问题可导致数据与预期不一致。最近在研究race conditions漏洞,用以下python 代码记录一下,以此论证,如下: from concurrent.futures import ThreadPoolExecutor globalNum 5 def test():global…

【深度分析】OpenAI o1是最强的推理模型,却不是最强模型!

大家好,我是木易,一个持续关注AI领域的互联网技术产品经理,国内Top2本科,美国Top10 CS研究生,MBA。我坚信AI是普通人变强的“外挂”,专注于分享AI全维度知识,包括但不限于AI科普,AI工…

一般在写SQL时需要注意哪些问题,可以提高查询的效率?

很多人写SQL按照自己喜好,没有规则意识,这对于自主查询影响不大,你爱怎么搞就怎么搞,一旦涉及到提交任务或团队共享,就不能乱写了,会浪费资源影响到开发效率,严重的甚至会服务器瘫痪。 提几个关…

python-在PyCharm中使用PyQt5

文章目录 1. 安装 PyQt5 和QtTools2. QtDesigner 和 PyUIC 的环境配置2.1 在 PyCharm 添加 Create Tools2.2 添加 PyUIC 工具 3. 创建ui界面4. 使用python调用ui界面参考文献 1. 安装 PyQt5 和QtTools QT 是最强大的 GUI 库之一,PyQt5 是 Python 绑定 QT5 应用的框…

idea一个窗口打开多个仓库的代码

一、背景 最近新进了一家外包公司,这个项目由于是微服务的,且每个微服务都独立用一个仓库进行代码管理。看项目的时候,我们不能一个窗口,只打开一个仓库代码,那样看起来会非常麻烦,一开始对项目全貌的了解…

Get包中的根组件

文章目录 1. 知识回顾2. 使用方法2.1 源码分析2.2 常用属性 3. 示例代码4. 内容总结 我们在上一章回中介绍了"Get包简介"相关的内容,本章回中将介绍GetMaterialApp组件.闲话休提,让我们一起Talk Flutter吧。 1. 知识回顾 我们在上一章回中已经…

多线程篇(Fork/Join)(持续更新迭代)

目录 知识大纲 一、简介 二、工作窃取算法 三、设计思想 步骤一:分割任务 步骤二:执行任务并合并结果 四、使用 五、异常处理 六、Fork/Join框架的实现原理 1. ForkJoinTask的fork方法实现原理 2. ForkJoinTask的join方法实现原理 七、源码剖…

Java-数据结构-二叉树-习题(三)  ̄へ ̄

文本目录: ❄️一、习题一(前序遍历非递归): ▶ 思路: ▶ 代码: ❄️二、习题二(中序遍历非递归): ▶ 思路: ▶ 代码: ❄️三、习题三(后序遍历非递归): ▶ 思路: …