数据结构八:线性表之循环队列的设计

       上篇博客,学习了栈,我们可以知道他也是一种线性表,遵从先进后出的原则,在本节,我们进一步学习另一种线性表—队列。就像饭堂里排队打饭的的队伍,作为一种先进先出的线性表,他又有哪些特别之处呢?又该如何应用呢?接下来,跟我走近队列的世界里。

目录

一、队列的应用场景

二、队列的基本概念和结构

2.1 队列的基本概念

2.2 队列的结构

2.3 队列的实现方式

三、循环队列栈的接口函数实现

3.0  循环队列设计的思想来源

3.0.1 从数组存放的角度分析

3.0.2 从时间复杂度的角度分析

3.1 循环队列的三个关键问题及如何解决?

3.2  循环队列的特点

3.3  循环队列的接口函数 

3.4  循环队列的设计(结构体)

3.5  循环队列的初始化

3.6 入队

3.7 出队

3.8 获取队头元素值

3.9 获取有效元素个数

3.10 判空

3.11 判满

3.12 扩容(无法直接扩容!循环队列的最致命缺陷)

3.13 打印

3.14 清空

3.15 销毁

四、总结


一、队列的应用场景

       队列是一种常见的数据结构,具有先进先出(FIFO)的特性,适用于许多场景,包括但不限于以下几个方面:

1. 任务调度: 队列可用于任务调度系统,例如处理异步任务或者在系统中处理任务队列。任务可以按照提交的顺序排队执行,确保公平性和顺序性。

2. 消息队列:消息队列是分布式系统中常见的通信模式,用于在不同组件或服务之间传递消息。生产者将消息推送到队列的尾部,而消费者则从队列的头部获取消息。这种模式在微服务架构中广泛应用,用于解耦各个服务之间的通信。

3. 缓冲区:队列常被用作缓冲区,用于在生产者和消费者之间进行数据传输。例如,计算机网络中的数据包可以在路由器或交换机上排队等待处理。

4. 广度优先搜索(BFS):在图论和树结构中,BFS算法常用队列来实现。它从起始顶点开始,先遍历其所有相邻的节点,然后逐层遍历其他节点,确保以最短路径访问所有节点。

5. 资源分配:队列可用于管理资源的分配,例如操作系统中的进程调度,或者服务器中的请求队列管理,确保资源按照特定规则分配给请求者。

6. 多线程编程:在多线程编程中,队列常被用作线程安全的数据结构,用于线程之间的通信和同步。一个线程可以将数据放入队列,而另一个线程则可以从队列中取出数据进行处理。

二、队列的基本概念和结构

2.1 队列的基本概念

      队列是一种先进先出(first in first out,FIFO)线性表,是一种常用的数据结构。

它只允许在表的一端(front)进行删除操作,而在表的另一端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。(队尾入队头出)队列中没有元素时,称为空队列。

2.2 队列的结构

  队尾中的元素遵从先进先出的原则,特别像是排队购票的过程。它的结构如下:

2.3 队列的实现方式

    队列的实现方式和栈类似,按照存储结构划分:基于顺序存储结构(数组存放)的循环队列、基于链式存储结构的链式队列。本节主要学习循环队列!

三、循环队列栈的接口函数实现

3.0  循环队列设计的思想来源

3.0.1 从数组存放的角度分析

       队列的顺序存储结构和顺序栈类似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外,还需要设置头尾两个指针front和rear,分别指示队列头元素及队尾元素的位置。(数组元素不动,让数组的下标变化)

我们规定:

  • 初始化建立空队列时,令front=rear=0
  • 每当插入新的队尾元素时,“尾指针增1”
  • 每当删除队头元素时,“头指针增1”
  • 在非空队列中,头指针始终指向队列头元素,尾指针始终指向队列尾元素的下一个位置

存在的问题:在入队和出队的操作中,头尾指针只增加不减小,致使被删除元素的空间永远无法重新利用,因此,尽管队列中实际的元素个数远远小于向量空间的规模,但也可能由于尾指针巳超出向量空间的上界而不能做入队操作,该现象称为假溢出!

         当进行动态创建队列的时候,也只不过是向后继续不断的申请内存空间,即时前面出队操作释放掉了前面的空间,但是指针依旧会向后进行移动,直到达到系统预留给程序的内存上界被强行终止,这对于极为频繁的队列操作和程序而言是致命的,这时候,就需要对我们的队列进行优化,使用更为优秀的结构——循环队列

解决办法:将顺序队列臆造为一个环状的空间,称之为循环队列

3.0.2 从时间复杂度的角度分析

         在设计顺序栈时,入栈和出栈的操作,数据都是通过尾插或者尾删进行的,很明显它的入栈和出栈时间复杂度都是O(1),在设计链式栈时,入栈和出栈的操作,数据都是通过单链表的头插和头删进行的,很明显它的入栈和出栈的时间复杂度也都是O(1),那么我们如何设计顺序队列,让队列也能达到O(1)的时间复杂度呢?即如何确定队头和队尾的位置?是放在数组的头部还是尾部?

      从上面的分析可知,如果只是简单的用一个动态数组存放队的数据,不管队头和队尾设计在哪一个位置,它的入队和出队的时间复杂度不可能同时达到O(1),因此,我们应该如何解决呢?    ——>既然数据动达不到时间复杂度的要求,那我们便换个思路,让数据不动,让数组元素的下标动,设置队头队尾两个指针front和rear,分别指示队列头元素及队尾元素的位置。这样每次插入和删除便不需要挪动数据,达到O(1)的时间复杂度要求。这便引入了循环队列的概念

3.1 循环队列的三个关键问题及如何解决?

①:如何让入队,出队时间复杂度都达到O(1)?

我们规定:

  • 初始化建立空队列时,令front=rear=0
  • 每当插入新的队尾元素时,“尾指针增1”
  • 每当删除队头元素时,“头指针增1”
  • 在非空队列中,头指针始终指向队列头元素,尾指针始终指向队列尾元素的下一个位置

②:怎么判满,怎么判空?

 

     从上面分析我们可以看到:判空与判满的条件一样,那到底是数组存满,还是数组是空数组,该如何区分呢? 主要采用第二种方法:浪费掉最后一个数组空间,以此对二者进行区分!如下所示:

        在队满时,队尾小于队头,条件rear+1=front是可以用来判断的,但是当队尾大于队头(越过最大下标和0下标),此时这个条件就不能在使用了,如下图所示队列已经满了:rear+1为6,而此时front等于0,二者不相等,这个条件不能作为判满的条件,那么又该如何修正呢?区域操作! 这样让队尾的值一值在0-maxsize之间,此时便不会越界,可以正常使用!

③怎么获取有效元素个数?

 

通过上面分析,求元素个数有两个公式,那么有没有办法可以统一上面两个公式?当然是有的。 

 总结:

3.2  循环队列的特点

循环队列的特点主要体现在以下几个方面:

  1. 大小固定:循环队列的大小是固定的,一旦定义就不能改变。当存储空间的最后一个位置已被使用,而再要进行入队运算时,只需要存储空间的第一个位置空闲,就可以将元素加入到第一个位置,即将存储空间的第一个位置作为队尾。
  2. 无溢出风险:循环队列通过队头和队尾两个指针来描述队列中的数据存储位置,可以有效地防止“假溢出”现象的发生。在实际应用中,当存储空间全满或者空时,都有front=rear的情况。通过这种方式,循环队列可以更简单、高效地解决顺序队列的“假溢出”问题。
  3. 高效的插入和删除操作:循环队列支持在队尾一端进行插入操作,而在队头一端进行删除操作。这一特性使得循环队列的插入和删除操作非常高效。
  4. 空间利用率高:循环队列通过队头和队尾两个指针连成一个环状的空间,充分利用了数组的空间。当存储空间的最后一个位置已被使用,而再要进行入队运算时,只需要存储空间的第一个位置空闲,就可以将元素加入到第一个位置,这样就可以减少内存的浪费,提高空间利用率。
  5. 应用场景广泛:由于其高效的插入和删除操作、空间利用率高以及能够动态调整队列大小的特性,循环队列在许多领域都有广泛的应用,如操作系统中的任务调度、通信协议中的数据包处理、线程池中的线程管理等。
  6. 循环队列的一个致命的缺陷就是:无法直接进行扩容!!!必须重新开辟内存空间!

3.3  循环队列的接口函数 

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "queue.h"


//初始化
void Init_Queue(struct Queue* que);

//入队
void Push(struct Queue* que, ELEM_TYPE val);

//出队
void Pop(struct Queue* que);

//获取队头元素值
ELEM_TYPE Front(struct Queue* que);

//获取有效元素个数
int Get_length(struct Queue* que);

//判空
bool IsEmpty(struct Queue* que);

//判满
bool IsFull(struct Queue* que);

//清空
void Clear(struct Queue* que);

//销毁
void Destroy(struct Queue* que);

//打印
void Show(struct Queue* que);

3.4  循环队列的设计(结构体)

     本质和顺序栈的设计差不多,只不过这里主要为3个成员,申请空间的指针(相当于顺序表中用来申请内存空间的指针)、队头指针(用来标记对头的位置),队尾指针(用来标记对尾的位置),简单起见,用整型变量front、rear即可,只不过循环队列无法直接扩容,因此不需要记录队列容量的变量capacity!

//循环队列的结构体设计
#define MAX_SIZE 10
typedef int ELEM_TYPE;

typedef struct Queue
{
	ELEM_TYPE *base;
	int front;   //队头指针
	int rear;    //队尾指针
}Queue, *PQueue;

3.5  循环队列的初始化

      循环队列在创建以后必须要进行初始化,否则内部为随机值,无法使用。循环队列的初始化主要是对结构体成员赋初值。核心就在于申请空间以及将front指针和rear指针内容赋值为0,即指向第0个元素即可(注意第 0个元素内容为空)。

//初始化
void Init_Queue(struct Queue* que)
{
    第0步:参数检测
    assert(que!=NULL);
    第1步:初始化赋值
	que->base = (ELEM_TYPE *)malloc(MAX_SIZE * sizeof(ELEM_TYPE));
	que->front = que->rear =0;
}

3.6 入队

     入队操作方法,直接将rear向后移动即可,但是要注意判断,如果rear达到了队列的空间上线,将要从头继续开始移动,这里推荐使用余数法,即无论如何求余都是在这片空间内进行操作,防止一次错误执行就直接整体崩溃,而且也相对而言更为简洁,不推荐使用if语句,这样显得比较累赘。注意进行加一移动位置操作的时候,不能直接q->rear++这样的操,这样计算机判断优先级会产生让自己意想不到的后果,此外这里还需要进行一次是否队列已满的判断,当我们rear指针的下一个位置就是front的位置的时候,即改循环队列已满。

//入队
void Push(struct Queue* que, ELEM_TYPE val)
{
    第0步:参数检测
    assert(que!=NULL);

	//1.判满
	if(IsFull(que))
	{
		return;
	}

	//2.给rear指向的下标赋值,进行入队操作
	que->base[que->rear] = val;

	//3.rear++(这里需要注意越界)
	que->rear = (que->rear+1)%MAX_SIZE;

}

3.7 出队

     循环队列的出队操作,直接将front进行后移一位即可,注意这时候有一个需要留意的地方,即队列是否为空,当队列为空的时候是无法进行出队操作的。

//出队
void Pop(struct Queue* que)
{
    第0步:参数检测
    assert(que!=NULL);
	//1.判空
	if(IsEmpty(que))
	{
		return;
	}

	//2.让队头指针向后挪动一下,但是注意越界
	que->front = (que->front+1)%MAX_SIZE;

}

3.8 获取队头元素值

直接返回front下标对应的数组元素即可!

//获取队头元素值
ELEM_TYPE Front(struct Queue* que)
{ 
    第0步:参数检测
    assert(que!=NULL);
   
	return que->base[que->front];
}

3.9 获取有效元素个数

直接利用公式计算,返回即可

//获取有效元素个数
int Get_length(struct Queue* que)
{
    第0步:参数检测
    assert(que!=NULL);
	return ((que->rear-que->front)+MAX_SIZE)%MAX_SIZE;
}

3.10 判空

直接用队列判空的条件即可!

//判空
bool IsEmpty(struct Queue* que)
{
    第0步:参数检测
    assert(que!=NULL);
	return que->front == que->rear;
}

3.11 判满

直接用队列判满的条件即可!

//判满
bool IsFull(struct Queue* que)
{
    第0步:参数检测
    assert(que!=NULL);
	return (que->rear + 1)%MAX_SIZE == que->front;
}

3.12 扩容(无法直接扩容!循环队列的最致命缺陷

       循环队列之所以无法在原地进行扩容,主要是因为在循环队列中,队列的元素是顺序排列的,而且循环队列的底层通常是通过数组来实现的。在数组中,元素是连续存储的,当数组空间不足以容纳更多的元素时,需要进行扩容,即分配一个更大的数组来存储元素。

但是,在循环队列中,元素的排列是循环的,即队列头部和尾部可能相邻,而队列的元素在数组中是连续存储的。当队列需要扩容时,如果直接在原数组上扩容,可能会因为数组的连续性导致无法满足队列头部和尾部相邻的条件,从而破坏了循环队列的特性。

        因此,为了实现循环队列的扩容,通常需要创建一个新的更大的数组,并将原数组中的元素按照顺序复制到新数组中,同时保持循环队列的循环特性。这种方式虽然需要额外的空间和时间来进行复制操作,但可以保证扩容后的循环队列仍然是正确的。

3.13 打印

从队头到队尾遍历所有元素打印即可!

//打印
void Show(struct Queue* que)
{
    第0步:参数检测
    assert(que!=NULL);
	for(int i=que->front; i!=que->rear; i=(i+1)%MAX_SIZE)
	{
		printf("%d ", que->base[i]);
	}
	printf("\n");

}

3.14 清空

直接让队头指针和队尾指针相等即可!

//清空
void Clear(struct Queue* que)
{
    第0步:参数检测
    assert(que!=NULL);
	que->front = que->rear = 0;
}

3.15 销毁

直接让队头指针和队尾指针相等,然后再释放即可!

//销毁
void Destroy(struct Queue* que)
{
    第0步:参数检测
    assert(que!=NULL);
	que->front = que->rear = 0;
	free(que->base);
	que->base = NULL;
}

四、总结

       以上便是我为大家带来的循环队列设计内容,若有不足,望各位大佬在评论区指出,谢谢大家!下一节继续进行链式队列的内容,感兴趣的你可以留下你们的点赞、收藏和关注,这是对我极大的鼓励,我也会更加努力创作更优质的作品。再次感谢大家!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

实现Spring底层机制(二)

文章目录 阶段2—封装bean定义信息到Map1.代码框架图2.代码实现1.文件目录2.新增注解Scope存储单例或多例信息Scope.java3.修改MonsterService.java指定多例注解4.新增bean定义对象存储bean定义信息BeanDefinition.java5.修改pom.xml增加依赖6.修改容器实现bean定义信息扫描Sun…

C语言|关于C语言变量的作用域、链接、存储期及相关知识(C多文件编程基础)

文章目录 作用域块作用域(block scope)函数作用域(function scope)函数原型作用域(function prototype scope)文件作用域(file scope)翻译单元和文件(作用域&#xff09; 链接(linkage)存储期(Storege Duration)静态存储期(static storage duration)线程存储期(thread storage …

kafka启动报错(kafka.common.InconsistentClusterIdException)

文章目录 前言kafka启动报错(kafka.common.InconsistentClusterIdException)1. 查找日志2. 定位问题/解决 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞的人每天的运气都不…

qt实现方框调整

效果 在四周调整 代码 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QWidget>class MainWindow : public QWidget {Q_OBJECT public:explicit MainWindow(QWidget *parent 0);~MainWindow();void paintEvent(QPaintEvent *event);void updateRect();void re…

ZYNQ--PL读写PS端DDR数据

PL 和PS的高效交互是zynq 7000 soc开发的重中之重,我们常常需要将PL端的大量数 据实时送到PS端处理,或者将PS端处理结果实时送到PL端处理,常规我们会想到使用DMA 的方式来进行,但是各种协议非常麻烦,灵活性也比较差,本节课程讲解如何直接通过AXI总 线来读写PS端ddr的数据…

什么是基尼系数

基尼系数是国际上用来综合考察居民内部收入分配差异状况的一个重要分析指标。每个人的收入有多有少&#xff0c;差距大时&#xff0c;基尼系数就高&#xff1b;差距小时&#xff0c;基尼系数就低。 一、基本概念 基尼系数表示在全部居民收入中&#xff0c;用于进行不平均分配…

补充centos7软件包的方式/编译安装源码包软件/企业案例/linux进程管理/企业管理进程系列命令(企业经验)--8820字详谈

cenros7软件包的安装方式 软件包分类安装方式优缺点rpm包软件开发商编译打包&#xff0c;安装简单&#xff0c;快速软件版本可能偏低&#xff0c;安装路径是固定好的源码包自己手动编译安装并且复杂软件爸爸随意选&#xff0c;可以定制安装路径二进制包解压就可以使用不能进行…

什么是AIGC技术

AIGC技术&#xff0c;即人工智能全局优化控制技术&#xff0c;是一种将人工智能与全局优化控制方法相结合的技术。它的主要目标是通过智能化的方法来解决复杂系统的优化问题&#xff0c;提高系统的性能和效率。 AIGC技术的主要目标是利用人工智能算法和技术来实现对系统整体的…

第三篇:Python编程基础:掌握核心语法与开发技巧

Python编程基础&#xff1a;掌握核心语法与开发技巧 1 引言 在这个信息化迅速蔓延的世界中&#xff0c;Python语言如同钥匙一般开启了通往各种可能性的大门。无论你是数据科学家、网络工程师、机器学习专家&#xff0c;还是仅仅对自动化办公感兴趣的办公室人员&#xff0c;Pyt…

【Elasticsearch<二>✈️✈️】基本属性概念与MySQL数据库的不同之处

目录 &#x1f378;前言 &#x1f37b;一、Elasticsearch 基本属性 1.1 ES VS MySQL 1.2 ES 属性概念 1.3 ES 的增删改查 &#x1f37a;二、自动补全场景 2.1 场景举例 2.2 使用数据分词器 2.3 查询的流程 2.4 整个查询流程图 &#x1f379;章末 &#x1f378;前言 上次初步…

Zynq 7000 系列中的Interconnect(互联)简介

PS&#xff08;处理器子系统&#xff09;内部的互联结构包含了多个交换机&#xff0c;用于通过AXI点对点通道连接系统资源。这些通道负责在主机和从机客户端之间进行地址、数据和响应事务通信。该互联结构负责管理多个待处理的事务&#xff0c;并且为Arm CPU设计了低延迟路径&a…

UE4_动画基础_FootIK

角色由于胶囊体的阻挡&#xff0c;双脚与地面平行&#xff0c;不会与斜坡、台阶等贴合&#xff0c;有一条腿会处于悬空状态&#xff0c;通过双骨骼IK节点使一只脚太高&#xff0c;让后胶囊体下降&#xff0c;修正双脚的角度。这就是逆向运动IK的方法。 一、新建第三人称模板游戏…

OpenStack云计算(十四)——综合演练手动部署OpenStack,

本项目的项目实训可以完全参考教材配套讲解的详细步骤实施&#xff0c;总体来说实训工作量较大&#xff0c;可根据需要选做&#xff0c;重点观看配套的微课视频。 项目实训一 【实训题目】 搭建OpenStack云平台基础环境 【实训目的】 掌握OpenStack基础环境的安装和配置方…

CTFshow-PWN-栈溢出(pwn36)

存在后门函数&#xff0c;如何利用&#xff1f; 好好好&#xff0c;终于到了这种有后门函数的了 checksec 检查一下&#xff1a; 32 位程序&#xff0c;RELRO 保护部分开启 RWX: Has RWX segments 存在可读可写可执行的段 使用 ida32 看 main 函数 跟进 ctfshow 函数…

C++系列-命名空间

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 命名空间 在C/C中&#xff0c;变量&#xff0c;函数和后面要学到的类都是大量存在的&#xff0c;这些变量&#xff0c;函数和类的名称都存在于全局作用域中&#xff0c;可能会导…

电机入门1

文章目录 122.12.22.3 33.13.23.33.4 1 2 2.1 电机板 驱动板电机分类 驱动器分类 转速 转向扭矩定时器 ADC 2.2 PID 自动控制 的核心闭环控制算是 PID的应用 2.3 无刷电机用的 可大大提高其控制效率 和控制精度 3 开发板的IO 电流太小了 20~25ma 电机要A 驱动板 信号放大没舵…

Linux防火墙相关命令以及ip白名单配置

Linux防火墙相关命令以及ip白名单配置 firewall防火墙基础命令查看防火墙的服务状态查看防火墙的状态服务的开启、关闭和重启查看防火墙规则端口的查询、开放和关闭重启防火墙 防火墙白名单配置部分参数介绍 firewall防火墙基础命令 查看防火墙的服务状态 systemctl status f…

乘数而上,创邻科技入选2024数商典型应用场景“乘数榜”

4月18日&#xff0c;由浙江省科学技术协会指导的2024未来数商大会在杭州成功举办。本次大会以“场景突破 乘数而上”为主题&#xff0c;国际国内数商共聚未来科技城学术交流中心&#xff0c;聚焦数据要素市场的制度创新、数据治理、场景应用与生态构建等话题展开研讨。 大会现…

C++入门基础(一)

目录 C关键字命名空间命名冲突例子域的概念理解命名空间定义例子1例子2例子3例子4例子5例子6例子7 C输出与输入输出输入 感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接 &#x1f412;&#x1f412;&#x1f412; 个人主页 &#x1f978;&#x1f978;&#x…

汽车底盘域的学习笔记

前言&#xff1a;底盘域分为传统车型底盘域和新能源车型底盘域&#xff08;新能源系统又可以分为纯电和混动车型&#xff0c;有时间可以再研究一下&#xff09; 1&#xff1a;传统车型底盘域 细分的话可以分为四个子系统 传动系统 行驶系统 转向系统 制动系统 1.1传动系…