详解循环队列——链表与数组双版本

        前言:本节内容主要是讲解循环队列。 在本篇中会讲到两个版本——数组版本、链表版本。本篇内容适合正在学习数据结构队列章节或者已经学过队列但对循环队列感觉模糊的友友们 。

首先先来看一下什么是循环队列

什么是循环队列

        因为是刚开始讲解, 所以我们要先来看什么是循环队列:循环队列就是首和尾相连接的队列。如图分别是链表和数组形式的循环队列:

        循环队列普通队列相同点是:都是从队尾进数据, 从队头出数据。

        循环队列普通队列不同点是: 普通队列的容量是动态的, 会根据数据的增加而增加。 但是循环队列的容量是静态的, 它不会随着数据的增加而增加。 当队满时, 我们如果想要再添加数据, 只有将对头的数据取出来才能再次添加数据。 循环队列也被成为: 环形缓冲器

数组版本

判断队空与队满

        博主认为对于一个数组版本的循环队列来说, 最重要的就是如何判断它的队空与队满。我们先来看一下如何判断队空和队空

        

        我们利用上图进行分析。 图中tail 和 head为两个指针。 队列中的数字不是存放的数据, 而是数组的下标索引。

        我们假设初始化队空的时候tail 和 head指向同一块空间。 那么因为此时队空,所以tail 和head指向的这块空间内没有数据。

        然后我们进行入队操作, 入队一个‘5’, 这个‘5’我用绿色用来表示存放的数据。如下图:

        既然, ‘5’入队, 那么tail一定要向后移动一位, 所以结果就是上图的情况。 接下来我们再进行入队操作, 依次入队‘6’、‘1’、‘8‘、’6‘、’1‘。这些元素入队后的情况如下图: 

        ok, 到了这一步请思考一下, 这个循环队列应该是此时为满, 还是应该再入队一个元素才满? 

        我们在这里进行假设如果再入队一个元素为满。 那么再入队一个元素后,假设这个元素为‘5’,循环队列的情况就是这个样子:

现在, 请将这两个图对比着看:

        要知道, 如果此时的状态为满, 那么判满的条件就是 tail == head ; 而判空的条件我们上面说到了也是 tail == head 。那么我们如何区分这个条件下到底是满还是空?所以我们的假设是错误的。真正为满的时候应该是这样的:

         

        这个时侯其实就是该循环队列队满的情况。 那么为什么会空出来一个位置, 这个位置怎么处理? 对于这个问题。 回答是循环队列的元素个数要比开的空间数小一。 当然有别的办法解决这个问题, 但是传统的数组循环队列, 这里就是这样处理的元素个数的最大容量要比开的空间数小一

入队和出队

        第二个重要的需要搞明白的就是对于数组循环队列来说的插入和删除。指针如何偏移的问题。 

        我们在上面画的这个圆是我们想象出来的逻辑结构, 而真正的数组应该是一串连续的空间。 如下图:

        那么如果我们给这个队列入数据, 当队满时真正的物理结构其实是这种情况:

        逻辑结构是这样的情况:

       

        然后我们出一次数据:

        这是物理结构:

            这是逻辑结构:

     

        从逻辑结构我们可以看出来循环队列这个时候已经不是队满了。 但是在真正的物理结构里我们如果入数据, 那么tail指针就会越界。 如何解决这个问题呢?

        这里用到的是一点数学的运算。 

        我们设这个循环队列的最大空间是数是:maxsize。那么我们只需要在每次入数据和出数据的时候让tail或者head模上一个maxsize就可以解决这个问题。 

        比如图中head == 6, maxsize == 7. 那么当head向后移动一位时, 再取模变成 (head + 1) % maxsize

        所以, 综上,当tail 指针或者 head指针在进行入队或者出队时, 要进行的操作是 : (tail + 1) % maxsize 或者 (head + 1) % maxsize

取对头和取队尾

        取对头比较简单, 因为head指针指向的位置就是对头的位置。如图:

这个时候我们直接取对头的数据 :

data[head];  //伪代码

但是取队尾就可能有问题。 就像上图,此时tail指向了索引为0的位置。 然后在逻辑结构上面我们看到只要 tail - 1 就可以拿到索引为6的队尾数据5。但是要知道, 上图的是逻辑结构。 这个循环队列真正的物理结构应该是一块连续的数组, 就像下图这样:

  

        这个时候我们直接取 data[tail - 1]就会越界访问,显然是不正确的。

        那么解决这个问题也是用取模的方法, 但是在取模的时候要先加上一个maxsize。也就是这样

int index = (tail - 1 + maxsize) % maxsize;
data[index];    //伪代码

        当tail - 1 >= 0的时候,加上maxsize 再模上maxsize相当于将加上的maxsize又消去了。 

        当tail - 1 < 0的时候, 加上maxsize就变成了小于maxsize的正数, 取模后还是它本身。 

 以上, 就是取对头和取队尾的需要注意的事项。

代码

        知道了上面的知识点后, 我们就可以着手用代码实现我们的循环队列了。 由于知识点已经讲过了, 所以代码直接贴图了。部分内容会有注释, 但不做讲解。


//重定义数据类型。 便于维护
typedef int SQDataType;typedef struct SeqQueue 
{SQDataType* _data;int _tail;int _head;int _maxsize;
}SQ; //初始化                     maxsize为循环队列的最大容量
void Init_SQ(SQ* sq, size_t maxsize) 
{sq->_maxsize = maxsize;sq->_data = (SQDataType*)malloc(sizeof(SQDataType) * maxsize);//sq->_tail = 0;sq->_head = 0;
}//判断队满 
bool judge_full(SQ* sq) 
{return (sq->_tail + 1) % sq->_maxsize == sq->_head;    //逻辑上tail + 1 == head为队满
}//判断队空
bool judge_empty(SQ* sq) 
{return sq->_tail == sq->_head;                         //当tail == head队空
}//入队列
void Push_SQ(SQ* sq, SQDataType data)                      
{//先判断队列是否为满, 如果满了就返回if (judge_full(sq)){printf("栈满\n");return;}sq->_data[sq->_tail] = data;sq->_tail = (sq->_tail + 1) % sq->_maxsize;               //入队列要取模
}//出队列
void Pop_SQ(SQ* sq) 
{//判断队列是否为空, 如果为空就返回if (judge_empty(sq))                                     {printf("栈空\n");return;}sq->_head = (sq->_head + 1) % sq->_maxsize;
}//取对头数据
SQDataType Top_SQ(SQ* sq) 
{//判断是否为空if (judge_empty(sq)){printf("栈空\n");exit(-1);}return sq->_data[sq->_head];
}//取队尾数据
SQDataType Back_SQ(SQ* sq) 
{//判断是否为满if (judge_empty(sq)){printf("栈空\n");exit(-1);}return sq->_data[sq->_tail - 1];
}

链表版本

入出队以及取队中数据

        其实循环队列链表版本要比数组版本更加麻烦。 首先我在这里先抛出几个问题:

        首先, 我们让这个循环队列的初始位置仍旧是tail == head。 那么它的队满位置由上面的学习我们知道是 tail->next == head;(下图中绿色数字表示节点中存放的数据)

           

这里有两个问题: 

  • 我们如何取到队尾的元素?
  • 我们如何进行出队和入队操作?出队需要释放节点吗? 如果释放节点, 那么入队时是不是需要申请节点?又或者我们直接偏移指针就可以? 

对于这两个问题, 我们先来思考一下第一个问题:

        想要取到队尾元素, 就要拿到tail指向节点的前一个节点。 那么就有两个办法解决这个问题——第一个办法就是创建一个前置指针指向tail的前一个节点, 如图:

        第二个办法就是弄成双向链表

这样取队尾就可以直接访问tail的前一个节点。 

        然后再思考第二个问题:我们在入队和出队的时候需要释放节点和申请节点吗?

        那么请看如果我们释放节点的时候会发生什么情况?如图是该循环队列的某个状态:

在这个状态下, 我们如果出队, 那么释放节点后让head指向下一个节点:

那么注意, 现在的容量减少了。 我们如果再进行入队, 假设我们入一个’5‘。 那么就变成了

好, 现在我们再对比一下这个状态和开始的状态:

        这两种状态, 是不是就是相当于head指针从一开始的指向1那个节点, 然后向后偏移一个节点。 而tail指针也相当于向后偏移了一个节点。 那么我们还有必要释放和申请节点来进行操作吗? 是不是只要偏移指针就可以了?这样是不是减少了申请和释放节点的成本, 更快更简便?

        所以, 综上, 我们就可以推断出, 链表的入队和出队操作我们不必要申请和释放节点, 和数组版本一样, 只要指针偏移即可 

循环队列的初始化

        链表循环队列另一个比较重要的要搞清楚的就是 这个队列的初始化结构是什么样的?

        其实,我们在初始化的时候将所有节点开好就可以, 然后让tail指针和head指针指向同一个节点。如图:

代码

        知道上面的所有注意事项后, 我们就可以设计链表循环队列了。 这里使用前置prevtail解决取队尾的问题。 如下为代码:

typedef int QDataType;typedef struct QueueNode
{QDataType _data;QueueNode* _next;
}QNode;struct Queue 
{QNode* _head;QNode* _tail;QNode* _prevtail;
};//链表队列初始化
void Init_Q(Queue* pq, int n) 
{pq->_head = NULL;pq->_tail = NULL;QNode* cur = NULL;while (n) {if (cur == NULL)  //创建第一个节点, head, tail , prev都指向第一个节点 {cur = pq->_head = pq->_tail = pq->_prevtail = new QNode;cur->_next = NULL;}else              //创建之后的节点{cur->_next = new QNode;cur = cur->_next;}n--;}cur->_next = pq->_head; //成环
}//判断空
bool Empty_Q(Queue* pq)
{return pq->_head == pq->_tail;
}//判断队满
bool Full_Q(Queue* pq)
{return pq->_tail->_next == pq->_head;
}//入队列
void Push_Q(Queue* pq, QDataType x) 
{if (Full_Q(pq)) {printf("栈满\n");return;}//将数据给tail,prev到tail的位置,tail向后偏移一位pq->_tail->_data = x;pq->_prevtail = pq->_tail;pq->_tail = pq->_tail->_next;
}//出队列
void Pop_Q(Queue* pq) 
{if (Empty_Q(pq)){printf("栈空\n");return;}//head指针向后移动一位即可pq->_head = pq->_head->_next;
}//取队头
QDataType Front_Q(Queue* pq) 
{if (Empty_Q(pq)){printf("栈空\n");return -1;}return pq->_head->_data;
}//取队尾
QDataType Back_Q(Queue* pq) 
{if (Empty_Q(pq)){printf("栈空\n");return -1;}return pq->_prevtail->_data;
}

--------------------------------------------------------------------------------------------------------------------------------

以上就是本节的全部内容。

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

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

相关文章

git知识总结

要知道 本地回退后&#xff0c;反悔了&#xff0c;可以恢复。前提是已经提交了&#xff0c;提交了就丢不了。 git reflog git reset --hard commitId 以前git push不让推&#xff0c;就是没有对应关系。第一次推要setxxx参数。 前奏 设置用户名和邮箱&#xff0c;设置错…

回溯之组合总和II

上一篇文章使用回溯解决了组合总和I&#xff0c;这次使用回溯解决组合总和II&#xff0c;下面先给出回溯的模板代码。 private void backtracking(参数1,参数2,...){if(递归终止条件){收集结果;return;}for(遍历集合){处理;backtracking(参数1,参数2,...); // 递归;回溯;} }组…

5. FactoryTalk View SE -- 模拟量趋势记录

step1&#xff1a; 在项目列表下找到数据记录–数据记录模型–新建。 step2&#xff1a;更改描述、文件标识符、存储格式。 step3&#xff1a;更改文件缓存路径。 step4&#xff1a;更改缓存文件保存的周期。 step5&#xff1a;5s保存一次数据。 step6&#xff1a;添加标…

从零开始搭建Springboot项目脚手架2:配置文件、返回值、日志等

1、多个环境与配置文件 2、统一返回值 返回值包括两种场景&#xff1a;正常controller的返回、异常发生之后返回 正常controller的返回&#xff1a;通过在controller的默认返回Response实现 异常发生之后返回&#xff1a;通过全局异常处理统一捕获返回 首先创建类StatusCode…

[Spring Cloud] (7)gateway防重放拦截器

文章目录 简述本文涉及代码已开源Fir Cloud 完整项目防重放防重放必要性&#xff1a;防重放机制作用&#xff1a; 整体效果后端进行处理 后端增加防重放开关配置签名密钥 工具类防重放拦截器 前端被防重放拦截增加防重放开关配置请求头增加防重放签名处理防重放验证处理函数bas…

打造清洁宜居家园保护自然生态环境,基于YOLOv5全系列【n/s/m/l/x】参数模型开发构建自然生态场景下违规违法垃圾倾倒检测识别系统

自然生态环境&#xff0c;作为我们人类赖以生存的家园&#xff0c;其健康与否直接关系到我们的生活质量。然而&#xff0c;近年来&#xff0c;一些不法分子为了个人私利&#xff0c;在河边、路边等公共区域肆意倾倒垃圾&#xff0c;严重破坏了环境的健康与平衡。这种行为不仅损…

18.04版本的ubuntu没有连接网络的图标(坑人版)

以下更新内核别看&#xff0c;因为后面安装驱动报一堆错误!!! 不升级内核成功方法跳转连接&#xff1a;https://blog.csdn.net/weixin_53765004/article/details/138771613?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%2213877…

单调栈问题

原理 单调栈的核心原理是&#xff1a;在栈内保持元素的单调性&#xff08;递增或递减&#xff09; 单调递增栈&#xff1a; 用于处理“下一个更小的元素”问题。当新元素比栈顶元素小或等于时&#xff0c;直接入栈&#xff1b;否则&#xff0c;一直从栈顶弹出元素&#xff0c…

OBS直播二次开发_OBS直播软件介绍

OBS工作室版 免费且开源的用于视频录制以及直播串流的软件。 下载以在Windows, Mac以及Linux上简单且快速的开始串流。 功能 实时高性能的视频/音频捕捉与混合,以及无限的场景模式使您可以通过自定义实现无缝转换。为视频源设计的滤镜例如图片蒙版,色彩校正,色度/色彩键控…

软件体系结构风格

目录 一、定义 二、.经典软件体系结构风格&#xff1a; 1.管道和过滤器 2.数据抽象和面向对象系统 3.基于事件系统&#xff08;隐式调用&#xff09; 4.分层系统 5.仓库 6.C2风格 7.C/S 8.三层C/S 9.B/S 题&#xff1a; 一、定义 软件体系机构风格是描述某一特定应用…

通过内网穿透实现远程访问个人电脑资源详细过程(免费)(NatApp + Tomcat)

目录 1. 什么是内网穿透 2. 内网穿透软件 3. NatApp配置 4. 启动NatApp 5. 通过内网穿透免费部署我们的springboot项目 通过内网穿透可以实现远程通过网络访问电脑的资源&#xff0c;本文主要讲述通过内网穿透实现远程访问个人电脑静态资源的访问&#xff0c;下一章节将讲…

C语言/数据解构——(随即链表的复制)

一.前言 嗨嗨嗨&#xff0c;大家好久不见。已经有好几天没更新了。今天我们就分享一道链表题吧——随即链表的复制https://leetcode.cn/problems/copy-list-with-random-pointer废话不多说&#xff0c;让我们直接开始今天的题目分享吧。 二.正文 1.1题目描述 他和单链表不同…

华为OD机试 - 求幸存数之和(Java 2024 C卷 100分)

华为OD机试 2024C卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测试…

机器学习面试篇

如何理解机器学习数据集的概念 数据集是机器学习的基础&#xff0c;它包括了用于训练和测试模型所需的数据。数据集通常以矩阵的形式存在&#xff0c;其中每一行代表一个样本&#xff08;或实例&#xff09;&#xff0c;每一列代表一个特征&#xff08;或属性&#xff09;。…

JVM从1%到99%【精选】-类加载子系统

目录 1.类的生命周期 1.加载 2.连接 3.初始化 2.类的加载器 1.类加载器的分类 2.双亲委派机制 3.面试题&#xff1a;类的双亲委派机制是什么&#xff1f; 4.打破双亲委派机制 1.类的生命周期 类加载过程&#xff1a;加载、链接&#xff08;验证、准备、解析&a…

数据与结构--堆

堆 堆的概念 堆&#xff1a;如果有一个关键码的集合K{k0,k1,k2,…,kn-1}&#xff0c;把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中&#xff0c;并满足ki<k2i1且ki<k2i2&#xff08;或满足ki>k2i1且ki>k2i2&#xff09;&#xff0c;其中i0,1,2,…

深度缓冲技术在AI去衣中的神奇作用

引言&#xff1a; 随着人工智能技术的飞速发展&#xff0c;其在图形处理和视觉领域的应用日益增多。AI去衣技术便是其中一个颇具争议但又技术上引人入胜的话题。今天&#xff0c;我们将深入探讨一项关键技术——深度缓冲&#xff08;Depth Buffering&#xff09;&#xff0c;它…

SpringAI 技术解析

1. 发展历史 SpringAI 的发展历史可以追溯到对 Spring 框架的扩展和改进&#xff0c;以支持人工智能相关的功能。随着人工智能技术的快速发展&#xff0c;SpringAI 逐渐成为 Spring 生态系统中的一个重要组成部分&#xff0c;为开发者提供了便捷、灵活的解决方案。 项目的灵感来…

如何用opencv去掉单元格的边框线,以提高Tesseract识别率?

在OpenCV中处理从表格切割下来的图片&#xff0c;并去掉单元格的边框线&#xff0c;以提升Tesseract的识别准确率&#xff0c;确实是一个具有挑战性的任务。在这种情况下&#xff0c;我们需要采取一种策略来预处理图像&#xff0c;使得数字与背景之间的对比度增强&#xff0c;同…

数据缓存,可以尝试RocksDB了

shigen坚持更新文章的博客写手&#xff0c;擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长&#xff0c;分享认知&#xff0c;留住感动。 个人IP&#xff1a;shigen shigen在最近的学习中&#xff0c;接触到了一款新的缓存数据库RocksDB&#xff…