【数据结构】和栈一样简单的结构——队列

news/2024/11/6 21:27:11/

【数据结构】和栈一样简单的结构——队列

  • 一、前言
    • 1、什么是队列?
    • 2、使用什么结构实现?
  • 二、目标
  • 三、实现
    • 1、初始化工作
    • 2、入队
      • 2.1、图解思路
      • 2.2、代码实现
    • 3、出队
      • 3.1、图解思路
      • 3.2、代码实现
    • 4、打印队列(用于测试)
    • 5、返回队头元素
    • 6、返回队尾元素
    • 7、返回队列中元素个数
    • 8、判断队列是否为空
    • 9、销毁队列

一、前言

1、什么是队列?

对于队列,百度百科上对它的简介是这样的:

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。

其实队列放到我们生活中也非常好理解,比如我们平时在食堂打饭或在超市里买东西付款的时候都需要排队。我们排的队肯定是后面来的人要排到队尾,前面的人先被服务到,也就是遵循了先进先出规则:
在这里插入图片描述
其实队列的实现也是和栈一样简单的,因为它的插入和删除也是规定死了的只能头删和尾插,所以我们主要要实现的也只是两个接口而已。

2、使用什么结构实现?

对于队列的实现主要使用的是两种结构,数组和链表:
在这里插入图片描述
那么选择哪一种结构更加方便一点呢?这里能明确地告诉大家是使用链表实现更加简单。因为要是使用数组尾做队尾的话入队是挺方便的,但是出队的话就要将后面的元素全都向前移动一个位置,选用数组头做队尾则反过来出队也要移动数据。不管怎么样都是会有一端不太方便。

而使用链表来实现的话,我们可以定义两个指针head和tail分别记录链表的头和为,然后入队和出队就只需要执行头删和尾插即可,两个方法的复杂度都是O(1)。

所以我们就选用链表来实现队列。

二、目标

队列主要要实现的功能如下:

// 队列的入队
void QueuePush(Queue* pq, QDataType x);
// 队列的出队
void QueuePop(Queue* pq);
// 打印队列(用于测试)
void printQueue(Queue* pq);
// 返回队列的对头元素
QDataType QueueFront(Queue* pq);
// 返回队列的队尾元素
QDataType QueueBack(Queue* pq);
// 返回队列中的节点个数
int QueueSize(Queue* pq);
// 判断队列是否为空
bool QueueEmpty(Queue* pq);
// 销毁队列
void QueueDestroy(Queue* pq);

三、实现

1、初始化工作

老规矩,我们还是要先做好前期工作,将要用到的各种结构先定义一下:

// 重定义数据类型
typedef int QDataType;// 定义节点类型
typedef struct QueueNode {struct QueueNode* next;QDataType data;
} QueueNode;

因为我们在队列中定义了两个指针head和tail,所以为了方便我们就再定义一个队列的类型,将这两个指针放到一个类型里,方便操作和维护:

// 定义队列类型
typedef struct Queue {QueueNode* head;QueueNode* tail;
} Queue;

然后就是对队列进行初始化,我们先将head和tail置空:

// 队列的初始化
void QueueInit(Queue* pq) {assert(pq);pq->head = NULL;pq->tail = NULL;
}

2、入队

2.1、图解思路

因为我们选择的是使用单链表来实现队列,所以入队的操作就基本和单链表的尾插一样了。又因为我们记录了链表的尾,所以我们就不在需要在重头找尾了,直接创建一个新节点,然后将其连接在tail的后面即可:
在这里插入图片描述
而仅有的特殊情况就是当我们的队列为空时候,我们需要将入head指针和tail指针同时指向这个入队的新节点:
在这里插入图片描述

2.2、代码实现

// 队列的入队
void QueuePush(Queue* pq, QDataType x) {assert(pq);// 创建一个新节点QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));if (NULL == newNode) {perror("malloc fail!\n");exit(-1);}newNode->data = x;if (NULL == pq->head) {pq->head = newNode;pq->tail = newNode;pq->tail->next = NULL;}else {pq->tail->next = newNode;pq->tail = pq->tail->next;pq->tail->next = NULL;}
}

3、出队

3.1、图解思路

出队对应的就是单链表的头删,其操作和单链表的头删一样,我们先用一个next指针保存队头的下一个节点,然后再释放队头节点,最后再将head指向next即可:
在这里插入图片描述
然后还有一个特殊情况就是当出队出到队列为空的时候,根据上面的逻辑,头指针head是已经为空的了。但我们发现尾指针tail还没被处理,也就是说尾指针还指向这已被释放表的节点,这就会导致野指针了:
在这里插入图片描述

所以当我们把队列删空的时候也要将尾指针tail给置空:
在这里插入图片描述

3.2、代码实现

// 队列的出队
void QueuePop(Queue* pq) {assert(pq);assert(!QueueEmpty(pq));QueueNode* next = pq->head->next;free(pq->head);pq->head = next;// 如果对头为空了,我们也要把队尾也给置空,避免野指针if (NULL == pq->head) {pq->tail = NULL;}
}

4、打印队列(用于测试)

然后我们再写一个用于测试的打印函数,这其实就是在打印链表啦:

// 打印队列
void printQueue(Queue* pq) {assert(pq);QueueNode* cur = pq->head;while (cur) {printf("[%d]——", cur->data);cur = cur->next;}printf("NULL\n");
}

打印和测试效果如下:
在这里插入图片描述

5、返回队头元素

这个其实就不用多说,如果队列不为空就直接返回即可:

// 返回队列的对头元素
QDataType QueueFront(Queue* pq) {assert(pq);assert(!QueueEmpty(pq));return pq->head->data;
}

6、返回队尾元素

这个也是一样:

// 返回队列的队尾元素
QDataType QueueBack(Queue* pq) {assert(pq);assert(!QueueEmpty(pq));return pq->tail->data;
}

7、返回队列中元素个数

对于队列中的元素个数,其实我们可以在定义队列类型的时候再额外定义一个size成员来记录当前队列中的元素个数,然后在我们执行入队和出队的时候相应的让size自加1或自减1。这样能省事不少:

// 定义队列类型
typedef struct Queue {QueueNode* head;QueueNode* tail;int size;
} Queue;

但是size这个属性好像在大多数情况下都用不到,所以若是单独再定义一个size好像有点怪怪的又好像有点多余,所以我们干脆就额外写一个函数来返回队列中的元素个数。
这个逻辑其实也很简单,就是遍历链表而已:

// 返回队列中的节点个数
int QueueSize(Queue* pq) {assert(pq);assert(!QueueEmpty(pq));QueueNode* cur = pq->head;int size = 0;while (cur) {size++;cur = cur->next;}return size;
}

8、判断队列是否为空

这个函数我们直接返回head是否等于空的判断结果即可:

// 判断队列是否为空
bool QueueEmpty(Queue* pq) {assert(pq);return pq->head == NULL;
}

9、销毁队列

销毁队列其实也就是销毁链表,这其实和销毁链表的逻辑是一样的,但我们最后还有注意也要讲head和tail给置空了:

// 销毁队列
void QueueDestroy(Queue* pq) {assert(pq);assert(!QueueEmpty(pq));QueueNode* cur = pq->head;QueueNode* next = cur->next;while (cur) {next = cur->next;free(cur);cur = next;}pq->head = NULL;pq->tail = NULL;
}

http://www.ppmy.cn/news/68053.html

相关文章

MySQL基础(二十九)数据库的设计规范

1 范式 1.1 范式简介 在关系型数据库中,关于数据表设计的基本原则、规则就称为范式。可以理解为,一张数据表的设计结 构需要满足的某种设计标准的 级别 。要想设计一个结构合理的关系型数据库,必须满足一定的范式。 1.2 范式都包括哪些 目…

Spring Data JPA简化数据访问,提升开发效率和程序可维护性

Spring Data JPA简化数据访问,提升开发效率和程序可维护性 一、Spring Data JPA简介1 JPA概述2 Spring Data JPA概述 二、Spring Data JPA与传统JPA的区别1 简化查询2 自定义查询方法3 分页和排序 三、Spring Data JPA的使用1 配置Spring Data JPA2 使用Spring Data…

在flutter中使用NFC(超全)

自建博客文章链接:https://www.heblogs.cn/articleDetails/644d1ea91978a16ab3471075 文章前景:目前公司主要的业务方向是sass平台,我们的admin系统是基于qiankun搭建的主基座和子模块,app是flutterh5。我主要负责的是 1、qiankun…

人工智能引发了科学研究的革命

人工智能引发了科学研究的革命 科学研究从第一,第二范式,升级到第三范式 趣讲大白话:人工智能成精了 【趣讲信息科技162期】 **************************** 国内顶尖的AI专家陆奇总结 科学研究的五个范式 1、经验主义(比如中医&am…

selenium+Java环境搭建

目录 ①下载Chrome浏览器并查看浏览器版本 ②下载解压Chrome浏览器驱动 ③配置Java环境 ④将驱动文件放到jdk的bin文件目录下 ⑤验证环境是否搭建成功 1、创建java(Maven)项目,在pom.xml中添加依赖 2、在java文件创建Main类 &am…

Java-Redis缓存穿透,击穿,雪崩和布隆算法

Java-Redis缓存穿透,击穿,雪崩和布隆算法 1.缓存穿透概念:2.如何解决缓存穿透:3.什么是缓存击穿?4.什么是缓存雪崩?5.导致缓存雪崩的原因:6.缓存穿透,缓存击穿,缓存雪崩的区别: 1.缓存穿透概念: 当一个用户想要查询数据时&…

Spring AOP 实践指南

Spring AOP 实践指南 文章目录 Spring AOP 实践指南一、概述1、简介2、官方资料3、本文档说明 二、基本使用1、引入依赖2、定义切面3、定义切点4、创建 HelloController5、启动项目,访问测试 三、通知1、概述五种通知通知的顺序 2、通知方法接受的参数3、前置通知代…

asl-fingerspelling比赛规则

每个参与者一个账户 您无法从多个帐户注册Kaggle,因此您无法从多重帐户提交。 团队之外没有私人共享 不允许在团队之外私下共享代码或数据。如果论坛上的所有参与者都可以使用代码,那么可以共享代码。 团队合并 团队合并是允许的,并且可以由团队负责人执行。为了合并,…