【数据结构】一文读懂循环队列的实现细节

news2024/12/29 10:42:47

循环队列最早出现在计算机系统设计中,它的出现主要是为了满足实际需求:在存储机制上,传统的队列存储方式难以满足一些实际应用中需要存储大量数据的场景。在有限的数组空间内,传统的队列存储方式可能会出现存储空间浪费过多、存储元素数量不够等问题。而循环队列由于可以利用数组空间的循环使用,从而更加高效地存储大量数据,因此更适合一些实际生产环境中的应用场景。

文章目录

  • 循环队列的概念
  • 循环队列的应用
  • 循环队列存储结构
  • 循环队列接口实现
    • 循环队列初始化
    • 检测循环队列是否为空
    • 检查循环队列是否已满
    • 循环队列队尾入队列
    • 循环队列队头出队列
    • 循环队列获取尾元素
    • 循环队列获取首元素
    • 销毁循环队列
  • 总结

循环队列的概念

  • 循环队列也是一种非常经典的数据结构,和普通队列类似,它也是一种先进先出(FIFO)的数据结构 , 原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。它使用数组来实现队列的数据储存和操作。循环队列的特点是当队列尾部到达数组的尽头时可以绕回队列的头部,在逻辑形式上看循环队列即形成了一个环形队列,从而达到了更高效的存储和操作效率。在循环队列中,队首和队尾指针可以循环移动,这样就可以利用数组的循环特性,实现空间的最大化利用。
    在这里插入图片描述

循环队列的应用

  • 循环队列在计算机领域中被广泛应用,尤其是在存储和数据传输方面。一些常见的应用场景包括
  1. 网络缓存

在网络通信中,往往需要处理大量的数据流。为了减轻短时间内的网络压力,可以使用循环队列作为缓存,缓冲传输过来的数据。

  1. 缓存处理

循环队列可以方便地创建一个缓冲区,用于在内存中缓存较大的数据量,从而在数据计算和处理中提高效率。

  1. 任务调度

在操作系统设计中,循环队列被广泛用于实现任务调度的功能。操作系统通常需要管理同步和并发执行的多个任务,而循环队列可以很好地维护这些任务的队列。

  1. 双向队列

循环队列除了可以实现单向队列,还可以实现双向队列。双向队列既可以从队列头部进入元素,也可以从队列尾部进入元素,将队列从一端变成了两端,更加灵活实用。

  • 总之,循环队列在操作数据流,任务调度,网络通信等众多领域中都有着广泛的应用。它具有高效存储和操作数据的特点,并且易于实现,是一种非常重要实用的数据结构。

循环队列存储结构

  • 循环队列的存储结构是基于一维数组实现的。但相对于普通的数组,循环队列将队列首尾相连,从逻辑形式形成了一个环形结构。在循环队列中,队列的长度是固定的,并且数组的下标也是固定的。
  • 使用数组来实现循环队列的存储结构有以下几个优点:
  1. 数组具有连续的存储空间,可以更好地利用内存。
  2. 数组能够提供随机访问数据项的能力,方便快捷,性能高效。
  3. 通过适当设置下标的规则,可以实现循环队列的各种操作,如入队、出队、判空、判满等。
  • 循环队列的存储结构中需要用一个一维数组,以及两个指针front 和 rear 来表示队头和队尾指针。其中,front 表示队头指针,它指向的是队头元素;rear 表示队尾指针,它指向的是队尾元素的下一个位置。因为循环队列是环形的,所以在使用队列时,我们需要始终注意指针的值和队列的长度之间的关系。
#define sz 8 //循环队列的最大空间
typedef struct Queue
{
	int *a; //指向队列空间的基址
	int front; //头指针
	int rear; //尾指针
	int k;	 //队列存储空间个数
}CircularQueue;
  • 循环队列与普通队列相比,最大的不同之处在于其队列的队首和队尾指针是可循环的。在循环队列中,队列为空的时候,队首和队尾指针相等;当队列满时,队尾指针必须为队首指针的前一个位置(考虑到环形队列的特点,实际上表现为队尾指针+1等于队首指针(后面实现接口会讲为什么))。故循环队列的空间利用率比普通队列的效率更高,在需要优化存储空间的场景下使用较多。
  • ! 循环队列的存储结构相对简单,但在使用时需要特别注意指针的值和队列长度的关系,避免出现指针指向越界等错误。那下面我们来实现他的接口吧。

循环队列接口实现

首先我们要了解该循环队列如何判空,或者判满?

当头指针和尾指针相等时该情况循环队列是满还是空呢?在这里插入图片描述

  • 解决方案
  1. 余留一个空间空置。满: rear + 1 == front 空:rear == front

为什么余留一个空间空置呢?因为这里使用了一个浪费一个空间的方法来处理循环队列。这个空间一般不存储数据,而是留给循环队列尾指针使用,以便尾指针跟踪队列的最后一个元素。在队列空时,此时头指针和尾指针都指向循环队列头部的位置,在队列满时,此时尾指针在最后一个元素的后一个位置,此时尾指针+1 就是等于头指针指向第一个元素。此时循环队列就是满了
在这里插入图片描述

  1. 增加一个size变量记录数据个数。空: size==0 满: size == 队列存储空间个数

循环队列初始化

  • 综上所述,循环队列在开辟空间时,应该开辟队列存储空间个数 + 1个,余留多一个空间空置,方便判空和判满.其他的都是基本的操作了.具体看代码注释.
//初始化循环队列
CircularQueue* CircularQueueCreate(int k) {
    CircularQueue* ret = (CircularQueue*)malloc(sizeof(CircularQueue)); //分配队列头空间
    if (!ret) //判断是否为空
    {
    	perror("malloc file");
        return NULL;
    }

    ret->a = (int*)malloc(sizeof(int) * (k + 1)); //分配队列存储空间(长度为k+1,浪费一个空间用于循环)
    ret->k = k;   // 设置队列容量
    ret->fornt = 0;   // 队首指针初始为 0
    ret->rear = 0;    // 队尾指针初始为 0

    return ret;  //返回新建的循环队列
}

检测循环队列是否为空

  • 当front和tail相同时,队列为空.
bool CircularQueueIsEmpty(CircularQueue* obj) {
    return (obj->fornt) == (obj->tail);
}

检查循环队列是否已满

  • 判断循环队列是否已满:尾指针real + 1,但是要注意的是循环队列为了实现环形存储,需要对循环队列长度k+1取模。 为什么呢?

使用取模就是为了实现循环的目的。例如:假设该队列的数组容量是 k,则当 rear 指针指向数组的第 k 个元素时,如果此时再往 rear 增加一个偏移量 1,就应该将 rear 指针指向数组的第 0 个元素。这时就需要使用取模,将 (rear + 1) % (k + 1) 的结果赋值给 rear 指针。然后判断是否相等.

假如rear尾指针指向循环队列最后一个空置位置,如果不取模,rear+1会导致数组越界,此时就失去循环的意义了。取模的意义就是为了让rear到了数组最后一个下标时+1回到第0个元素。例如: 尾指针是指向下标最后一个元素5的 5+1 % 5 + 1 = 0。
在这里插入图片描述

bool CircularQueueIsFull(CircularQueue* obj) {
    return (obj->rear + 1) % (obj->k + 1) == obj->fornt;
}

循环队列队尾入队列

  • 在往循环队列中添加元素时,需要先判断队列是否已满,然后在尾指针后面添加元素,并将尾指针向后移动一位,最后对尾指针进行取模操作,以实现循环队列的效果。
bool CircularQueuePush(CircularQueue* obj, int value) {
    if (CircularQueueIsFull(obj)) {  // 判断队列是否已满
        return false;   // 返回 false 表示添加元素失败
    } else {  // 否则说明队列未满
        obj->a[obj->tail] = value;  // 在队尾添加元素
        obj->rear++;  // 队尾指针往后移动一位
        obj->rear %= (obj->k+1);  // 更新队尾指针,实现循环队列
        return true;   // 返回 true 表示添加元素成功
    }
}
  • obj->rear %= (obj->k+1)这个操作是为了实现循环队列,因为队列的长度是固定的,如果尾指针超过了队列的长度,就需要将它重新回到队列的开始处。其实跟上面判满的取模的逻辑差不多.

我们再举个例子,假设队列的长度为 k+1=6,且数组下标从 0 开始。初始条件下,队列为空,头指针和尾指针都指向位置 0。当我们首次插入元素时,尾指针会移动到下标 1 的位置,而当我们再次插入元素时,尾指针会移动到下标 2 的位置,以此类推。当尾指针指向下标 5 的位置,即队列的最后一个位置时,我们需要将尾指针再向前移动一位,并将其指向下标 0 的位置,从而实现循环队列的效果。这个操作就是通过取模的方式来实现的,即每当尾指针大于等于队列长度时,我们将它除以队列长度后的余数赋值给尾指针,从而让尾指针重新回到队列的开始处。


循环队列队头出队列

  • 循环队列删除对头数据时,先判断是否为空,如果为空就直接返回无需删除,不为空就将头指针往后移动一位,并对新的头指针做取模操作以实现循环队列。
bool CircularQueuePop(CircularQueue* obj) {
    if (CircularQueueIsEmpty(obj)) {  // 判断队列是否为空
        return false;   // 返回 false 表示删除元素失败
    } else {  // 否则说明队列非空
        obj->front++;   // 头指针向后移动一位
        obj->front %= (obj->k+1);  // 对新的头指针做取模操作,实现循环队列
        return true;   // 返回 true 表示删除元素成功
    }
}
  • 为什么向后移动一位就是删除呢?

循环队列中,队列的头尾指针指向的是环形数组中的某一个元素,队列中的元素都是按照插入顺序排列在头尾指针之间的。因此,队列的删除操作其实就是移动头指针指向下一个元素,也就是所谓的“出队”操作。在队列中删除元素不会真正地删除那个元素,而是将头指针向后移动一位并指向下一个元素,这样就实现了元素的逻辑删除。

  • obj->front %= (obj->k+1) 为什么要取模?

这个取模操作,和添加元素中所使用的一样。这个操作的目的是让头指针重新回到队列的开始处。具体来说,如果头指针 front 指向数组的最后一个位置,再次执行删除操作时,我们需要将头指针向后移动一位,并将其指向数组的第一个位置,从而循环利用数组的位置。所以,在移动头指针时,需要对其进行取模操作,以保证头指针始终指向有效的数组元素。


循环队列获取尾元素

  • 该代码具体流程是:如果队列为空,则返回 -1,否则返回尾指针所指位置的值。
int CircularQueueRear(CircularQueue* obj) {
    if (CircularQueueIsEmpty(obj)) {  // 判断队列是否为空
        return -1;  // 如果队列为空,则返回 -1
    } else {  // 否则说明队尾存在元素
        return obj->a[(obj->rear + obj->k) % (obj->k + 1)];  // 返回尾指针所指位置的元素值
    }
}
  • return obj->a[(obj->rear + obj->k) % (obj->k + 1)]; 如何理解?

其实该代码是返回 rear之前的一个元素值。下面我讲用图来解答.

当rear指向了下标5的位置,我们应该返回下标下标为4的位置。
例如:当rear为5时, 5+5%6 = 4. 返回了数组下标为4的元素.

在这里插入图片描述

  • 可能这里有的人会有疑问? 我直接返回return [rear-1] 不就可以了吗,还用取模那么麻烦吗?那接下来看看这种情况

如果当rear是0时,直接return [rear-1] 返回数组下标-1不就造成越界访问了?所以我们的取模妙就妙在这里,当rear为0时,0+5 % 6 =5 , 直接返回数组下标为5的尾元素。
在这里插入图片描述


循环队列获取首元素

  • 如果队列为空,则返回 -1,否则返回队首元素的值。
int CircularQueueFront(CircularQueue* obj) {
    if (CircularQueueIsEmpty(obj)) {  // 判断队列是否为空
        return -1;  // 如果队列为空,则返回 -1
    } else {  // 否则说明队首存在元素
        return obj->a[obj->fornt];  // 返回队首元素的值
    }
}

销毁循环队列

  • 该代码实现简简单,直接看代码注释吧.
void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);  // 释放队列中元素所占用的内存空间
    obj->k = 0;  // 将队列长度设为 0
    obj->fornt = 0;  // 将队首指针设为 0
    obj->tail = 0;  // 将队尾指针设为 0
    free(obj);  // 释放循环队列结构体所占用的内存空间
}

总结

  • 循环队列的存储结构相对简单,但在使用时需要特别注意指针的值和队列长度的关系,避免出现指针指向越界等错误. 循环队列的精髓就在于取模,只要把取模控制好,相信大家对循环队列已经了如指掌了.

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

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

相关文章

【操作系统】线程简介

线程简介 线程概念 在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。 所以,线程是轻量级的进程(LWP:light w…

4.1 - 信息收集 - 子域名收集

「作者简介」:CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」:对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 子域名收集 一、域名爆破工具二、搜索引擎1、百度2、必应 三、第三方网站1、VirusTotal2、…

LLaMA模型系统解读

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

深度剖析,如何从底层代码层面理解Selenium和Appium的关联

目录 前言: 一、Selenium和WebDriver 二、Appium和WebDriver 三、Selenium和Appium的底层关联 1. Selenium WebDriver提供底层的浏览器控制机制 2. 利用JSON Wire Protocol通信协议实现通讯机制 四、实例代码 总结: 前言: Selenium和…

FFmpeg命令实战(中)

标题 1.ffplay命令播放2.ffplay简单过滤器3 .ffmpeg命令参数1.主要参数2. 音频参数3.视频参数 4.ffmpeg命令提取音视频数据1.保留封装格式2.提取视频3.提取音频 5.ffmpeg提取像素格式1.提取YUV2.提取RGB3.提取PCM 5.ffmpeg命令转封装格式1.保持编码格式2.改变编码格式3.修改帧率…

String源码

介绍 1&#xff09;String 是一个 final 类&#xff0c;即不能被继承的类 。 2&#xff09;String类实现了 java.io.Serializable 接口&#xff0c;可以实现序列化。 3&#xff09;String类实现了 Comparable< String>&#xff0c;可以用于比较大小&#xff08;按顺序…

49天精通Java,第34天,finalize、引用计数、JVM停止复制、JVM即时编译器

目录 一、finalize二、引用计数三、JVM停止复制四、JVM即时编译器五、惰性评估 大家好&#xff0c;我是哪吒。 &#x1f3c6;本文收录于&#xff0c;49天精通Java从入门到就业。 全网最细Java零基础手把手入门教程&#xff0c;系列课程包括&#xff1a;基础篇、集合篇、Java8…

chatgpt赋能Python-pythonchallenge

Python Challenge: 挑战你的Python技能 如果你正在寻找一种提高Python编程技能的有趣方法&#xff0c;那么Python Challenge是一个不错的选择。Python Challenge是一个在线的puzzle游戏&#xff0c;每个挑战都需要使用Python编写程序来解决。这些挑战是由一个名叫Nadav Samet的…

centos或ubuntu部署OpenSips

参考 Centos7安装opensips超详细教程 centos7 部署opensips信令服务器 【死磕opensips】sip协议解析 开源SIP Kamailio OpenSIPS的四种均衡负载算法详解和SBC呼叫路由 基于SIP协议的性能测试——奇林软件kylinPET OpenSIPS实战&#xff08;一&#xff09;&#xff1a;OpenSIPS…

【消息中间件】RocketMQ如何保证消息的可靠性?

文章目录 前言一 、发送端消息可靠性1. 同步发送2. 异步发送3. 单向发送4. 发送重试策略 二、存储端消息可靠性1. 存储可靠性挑战2. 同步刷盘3. 异步刷盘&#xff08;默认&#xff09;4. 过期文件删除 三、消费端消息可靠性1. 消费重试2. 死信队列3. 消息回溯 四、总结参考与感…

一文读懂“大语言模型”

1、背景 本文基于谷歌云的官方视频&#xff1a;《Introduction to Large Language Models》 &#xff0c;使用 ChatGPT4 整理而成&#xff0c;希望对大家入门大语言模型有帮助。 本课程主要包括以下 4 方面的内容&#xff1a; 大语言模型的定义描述大语言模型的用例解释提示…

网络安全分组混战靶机攻击与加固——BPlinux系列

网络安全分组混战靶机攻击与加固——BPlinux系列 目录 一、渗透过程 二、加固过程 三、中职网络安全竞赛知识星球 一、渗透过程 #这是一个以前混战阶段用的靶机然后C模块也会用 1、我们先使用nmap --scriptvuln(这是nmap自带的脚本&#xff0c;可以扫描可利用的漏洞&…

计算机网络|第四章:网络层:数据平面

前文回顾&#xff1a;第三章&#xff1a;传输层 运输层依赖于网络层的主机到主机的通信服务&#xff0c;提供各种形式的进程到进程的通信。网络层与传输层和应用层不同的是&#xff0c;在网络中的每一台主机和路由器中都有一个网络层部分。正因如此&#xff0c;网络层协议是协议…

metaRTC+ZLMediaKit实现webrtc的推拉流

概述 ZLMediaKit是一个基于C11的高性能运营级流媒体服务框架&#xff0c;是一个支持webrtc SFU的优秀的流媒体服务器系统。 metaRTC新版本支持whip/whep协议&#xff0c;支持whip/whep协议的ZLMediaKit推拉流。 信令通信 ZLMediaKit新版本支持whip和whep协议&#xff0c;支…

065:cesium设置带有箭头的线材质(material-9)

第065个 点击查看专栏目录 本示例的目的是介绍如何在vue+cesium中设置带有箭头的线材质,请参考源代码,了解PolylineArrowMaterialProperty的应用。 直接复制下面的 vue+cesium源代码,操作2分钟即可运行实现效果. 文章目录 示例效果配置方式示例源代码(共82行)相关API参考…

Microsoft Office 2010安装

哈喽&#xff0c;大家好。今天一起学习的是office2010的安装&#xff0c;有兴趣的小伙伴也可以来一起试试手。 一、测试演示参数 演示操作系统&#xff1a;Windows 7 不建议win10及以上操作系统使用 系统类型&#xff1a;64位 演示版本&#xff1a;SW_DVD5_Office_Profession…

Ceres简介及示例(9)On Derivatives(Numeric derivatives)

使用analytic derivatives的另一个极端是使用numeric derivatives。关键是&#xff0c;对函数f(x)关于x的求导过程可以写成极限形式: Forward Differences前向差分 当然&#xff0c;在计算机中&#xff0c;我们不能执行数值求极限操作&#xff0c;所以我们要做的是&#xff0…

squid的基本代理

一、Squid代理服务器的概述 squid 作为一款服务器代理工具&#xff0c;可以缓存网页对象&#xff0c;减少重复请求&#xff0c;从而达到加快网页访问速度&#xff0c;隐藏客户机真实IP&#xff0c;更为安全。 Squid主要提供缓存加速、应用层过滤控制的功能 1、squid代理的工…

攻击者使用“Geacon”Cobalt Strike工具瞄准macOS

威胁行为者现在正在部署一种名为 Geacon 的 Cobalt Strike 的 Go 语言实现&#xff0c;它于四年前首次出现在 GitHub 上。 他们正在使用红队和攻击模拟工具来针对 macOS 系统&#xff0c;其方式与过去几年在 Windows 平台上使用 Cobalt Strike 进行后开发活动的方式大致相同。…

Rust每日一练(Leetday0007) 删除结点、有效括号、合并链表

目录 19. 删除链表的倒数第 N 个结点 Remove-nth-node-from-end-of-list &#x1f31f;&#x1f31f; 20. 有效的括号 Valid Parentheses &#x1f31f; 21. 合并两个有序链表 Mmerge-two-sorted-lists &#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Ru…