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

news2024/12/28 22:30:54

文章目录

  • 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;
	}
	//如果队列为空,要让头结点和尾节点都指向新节点
	else
		q->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或0
	q->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) {
	//找有数据的队列,都没有就用 q1
    QU* 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;
    }
    // 2
    while (use->head->next) {
        PushToBack(other, PeekFromFront(use));
    }
    // 3
    return 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.coloradmin.cn/o/2135362.html

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

相关文章

计算机视觉(二)—— MDPI特刊推荐

特刊征稿 01 期刊名称: Applied Computer Vision and Pattern Recognition: 2nd Volume 截止时间: 摘要提交截止日期:2024年10月30日 投稿截止日期:2024年12月30日 目标及范围: 包括但不限于以下领域&#xff1a…

C++:线程库

C&#xff1a;线程库 threadthreadthis_threadchrono 引用拷贝问题 mutexmutextimed_mutexrecursive_mutexlock_guardunique_lock atomicatomicCAS condition_variablecondition_variable thread 操作线程需要头文件<thread>&#xff0c;头文件包含线程相关操作&#xf…

上班炒股会被开除吗?公司是如何发现员工上班炒股?一文告诉你答案!

随着互联网金融的发展&#xff0c;股票交易变得越来越便捷&#xff0c;不少上班族选择利用工作之余的时间来进行股票投资。 然而&#xff0c;这种行为是否合规&#xff1f;公司又是如何发现并处理这种情况的呢&#xff1f;本文将为您解答这些问题。 一、上班炒股是否合规&…

JAVA毕业设计175—基于Java+Springboot+vue3的医院预约挂号管理系统(源代码+数据库)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringbootvue3的医院预约挂号管理系统(源代码数据库)175 一、系统介绍 本项目前后端分离(可以改为ssm版本)&#xff0c;分为用户、医生、管理员三种角色 1、用户&#x…

交换机最常用的网络变压器分为DIP和SM

华强盛电子导读&#xff1a;交换机通用网络变压器插件48PIN最为常见 您好&#xff01;今天我要给您介绍一款真正能为您的工业生产带来变革的产品——华强盛工业滤波器。在如今这个高度数字化的工业时代&#xff0c;可靠的网络连接至关重要&#xff0c;而华强盛工业网络变压器就…

Smartbi体验中心新增系列Demo,用户体验更丰富

为进一步提升用户体验&#xff0c;让大家更直观地了解Smartbi产品在数据分析方面的功能优势&#xff0c;Smartbi体验中心近期新增了一系列Demo。这些更新旨在优化产品操作流程&#xff0c;并为用户提供更多真实场景下的应用参考。接下来&#xff0c;我们一起简要浏览此次体验中…

KEIL仿真时弹窗 “Cannot access target.”

现象 仿真时&#xff0c;点击暂停就会弹出下图窗口。 Cannot access target. Shutting down debug session. 解决方法 开启STM32的Debug&#xff0c;如下图。

基于WIFI的开关控制器设计与实现

本设计基于STC89C52RC单片机设计的WiFi开关器系统&#xff0c;旨在通过软硬件设计实现按键和手机APP远程同步控制4个继电器驱动3个LED灯、1个风扇&#xff0c;并且具备时间显示、开关状态显示、定时驱动&#xff0c;时间设定及保存等功能。该设计在硬件方面采用STC89C52单片机作…

软考中项(第三版) 项目成本管理总结

前言 系统集成项目管理工程师考试&#xff08;简称软考中项&#xff09;&#xff0c;其中案例分析也是很大一部分考试内容&#xff0c;目前正在学习中&#xff0c;现总结一些可能会考到的知识点供大家参考。 1.1、项目成本管理总线索 1、项目成本失控的原因 &#xff08;1&a…

python库安装失败问题

pip install XXXX 报错信息如下 D:\Dev>pip install D:\Dev\robotlib-0.0.33.tar.gz DEPRECATION: Loading egg at d:\app\dev\python\lib\site-packages\fs11a3_package-1.3-py3.11.egg is deprecated. pip 24.3 will enforce this behaviour change. A possible replace…

【机器学习】使用Numpy实现神经网络训练全流程

文章目录 网络搭建前向传播反向传播损失计算完整代码 曾经在面试一家大模型公司时遇到的面试真题&#xff0c;当时费力写了一个小时才写出来&#xff0c;自然面试也挂了。后来复盘&#xff0c;发现反向传播掌握程度还是太差&#xff0c;甚至连梯度链式传播法则都没有弄明白。 网…

solidity-19-fallback

接收ETH receive和fallback receive和callback是solidity中两个特殊的回调函数&#xff0c;一个处理接收ETH,一个处理不存在的函数调用。本质上就是吧fallback拆成了两个回调函数。我暂时不知道什么是fallback fallback调用不存在的函数时会被调用也就是这个函数是不是等价于…

视频转音频,分享这六种转换操作

视频转音频&#xff0c;随着多媒体技术的发展&#xff0c;人们越来越频繁地需要将视频中的音频部分提取出来单独使用。无论是为了制作播客、获取音乐片段还是其他需求&#xff0c;视频转音频都是一项非常实用的技能。为了让你轻松应对各种场合的需求&#xff0c;下文将为你详细…

day-55 不同路径

思路 动态规划&#xff1a;因为只能向右或向下移动&#xff0c;可以得出状态转换方程&#xff1a;dp[i][j]dp[i-1][j]dp[i][j-1] 解题过程 直接令第一行和第一列全为1&#xff0c;然后通过状态转换方程进行计算&#xff0c;返回dp[m-1][n-1]即可 Code class Solution {publi…

Centos挂载和删除nfs

一、Centos挂载nfs 1、安装NFS客户端软件 sudo yum install nfs-utils 2、 创建一个挂载点目录 mkdir -p /mnt/nfs 注意:目录可以随意创建 3、永久挂载nfs 即系统在每次启动后自动挂载NFS共享 (1)编辑 /etc/fstab vim /etc/fstab (2)添加nfs <nfs_server_ip&…

AI算法盒如何精准守护你的安全区域

在当今智能化时代&#xff0c;安全防范已成为社会各个领域的核心需求之一。万物AI算法盒&#xff0c;作为前沿科技的集大成者&#xff0c;其内置的区域人员入侵检测视觉算法&#xff0c;以卓越的性能和广泛的应用场景&#xff0c;为各行各业提供了高效、精准的安全解决方案。 核…

使用iperf3测试局域网服务器之间带宽

文章目录 一、下载安装1、windows2、centos 二、使用0、参数详解1、centos 一、下载安装 1、windows https://iperf.fr/iperf-download.php 拉到最下面选最新版&#xff1a; 2、centos yum install iperf3二、使用 0、参数详解 服务器或客户端&#xff1a; -p, --port #…

喧嚣漫天之际,重新审视以太坊的定位与路线图

价值捕获很重要&#xff0c;但现在讨论为时尚早。 作者&#xff1a;Mike Neuder&#xff08;以太坊基金会研究员&#xff09;&#xff1b;译者&#xff1a;Azuma&#xff1b;编辑&#xff1a;郝方舟 出品 | Odaily星球日报&#xff08;ID&#xff1a;o-daily&#xff09; 编者按…

Centos7通过Docker安装openGauss5.0.2并配置用户供Navicat连接使用

下载镜像 [rootiZ2ze3qc9ouxm10ykn3cvdZ ~]# docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/enmotech/opengauss:5.0.2 5.0.2: Pulling from ddn-k8s/docker.io/enmotech/opengauss 2ec76a50fe7c: Pull complete e48b50219b49: Pull complete 512e203af4…

万龙觉醒免费脚本,自动打金挂机!VMOS云手机辅助攻略!

《万龙觉醒》作为一款策略类手游&#xff0c;玩家需要在多个方面进行资源管理和战斗部署。为了更加高效地进行游戏&#xff0c;推荐使用VMOS云手机。通过VMOS云手机&#xff0c;你可以体验到游戏专属定制版的云手机&#xff0c;它内置游戏安装包&#xff0c;省去了重新下载安装…