【数据结构】环形队列(循环队列)学习笔记总结

文章目录

在计算机科学中,数据结构是组织和存储数据的方式,它对于高效的算法设计至关重要。队列是一种常见的数据结构,遵循 FIFO (先进先出,First-In-First-Out)的原则。然而,在实际应用中,传统的线性队列可能会遇到一些问题,例如当队列满时无法再插入新元素,即使队列头部有空闲空间。为了解决这些问题, 环形队列 (也称为 循环队列 )应运而生。

环形队列通过将数组的末端与开头连接起来形成一个循环结构,从而更有效地利用内存空间。这种数据结构在许多应用场景中都非常有用,包括缓冲区管理、生产者-消费者问题、日志记录、消息队列等。本文是我学习环形队列时所作笔记的总结,并使用 C 语言实现一个高效且功能丰富的环形队列,以及探讨其在实际项目中的应用。

什么是环形队列?

为充分利用向量空间,克服 " 假溢出^1^" 现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。

------引用至《循环队列_百度百科

要理解环形队列,可以先从单个变量的读写开始。

以串口收发数据为例,假设某些外设通过串口向主机发送 1 byte 的数据,且每秒发送一次,那么这 1 byte 的数据会先存在主机的接收缓冲存储区中。假设主机的串口接收缓冲存储区大小也刚好为 1 byte,一秒的间隔时间足够主机将缓冲存储区的数据及时读走并处理。

但是,如果发送的数据不止 1 byte,而是多个字节的情况,那么可能就会存在某个数据还没被读取,接收缓冲存储区就被下一个数据覆盖的情况。这时可以用中断的方式来处理这种情况,只要一数据存入接收缓冲存储区,就立刻触发中断把数据取走,存入其他存储区。不过,这样也有一个问题,如果上一个数据还没处理完毕,也会被中断程序覆盖为下一个数据,同样存在数据丢失的情况。

解决这个问题的办法也很简单,用一个相对较大的存储区,就可以存放更多的还没来得及处理的数据。根据先收到的数据先处理的原则,就可以用顺序队列作为这个存储区。

顺序队列有队头和队尾,而且队头和队尾的位置是可以变化的,所以需要设置两个指针 frontrear 分别指示队头元素和队尾元素在向量空间中的位置,它们的初值在队列初始化时均应设置为 0。

新元素出入队列的动作称为入队 ,入队时将新元素插入 rear 所指的位置,然后将 rear 加 1,即指向下一个没有数据的空间。元素从队列被读出的动作成为出队 ,出队时,读取 front 所指的元素后将元素删去,然后将 front 加 1 并,即指向下一个待处理数据的空间。

使用顺序队列后,即使来不急处理的数据,也能被逐个存储起来等待处理。但是,仍然存在一些弊端,根据顺序队列的特点,如果队列有元素,队头指针始终指向队头元素,队尾指针始终指向队尾元素的下一位置。所以当头尾指针相等时,队列为空。那么就会存在一种这样的情况(如下图所示),队列的空间并不是无限大的,当 rear 等于队列总长时,说明队列已经 "满" 了。此时若再执行入队操作,便会出现队满 "溢出"。然而,由于在此之前可能也执行了若干次出队操作.因而队列的前面部分可能还有很多闲置的元素空间,即这种溢出并非是真的没有可用的存储空间,故称这种溢出现象为 "假溢出"。

如果还有数据要入队但又无法入队,势必会造成数据丢失的情况,因此要一定解决队列这种 "假溢出" 的问题,否则使用队列就没有太多价值。

队列的 "假溢出" 现象,是因为又很多空间没有利用到,那么克服这种现象的办法就是:将队列空间想象为一个首尾相接的圆环,并称这种队列为环形队列,也叫循环队列。

循环队列就是将队列存储空间的最后一个位置绕到第一个位置,形成逻辑上的环状空间,供队列循环使用。在循环队列结构中,当存储空间的最后一个位置已被使用而再要进入队运算时,只需要存储空间的第一个位置空闲,便可将元素加入到第一个位置,即将存储空间的第一个位置作为队尾,可以更简单防止 "假溢出" 的发生。

更加形象的表示方法如下图所示,环形队列同样需要两个指针分别表示队头(head 指针)和队尾( tail 指针),与顺序队列的队头和队尾大致相同,入队时由队尾 tail 指针写入,并向后偏移;出队时由队头 head 指针取出后删除,并向后偏移。

环形队列有一种特殊情况与顺序队列不同,顺序队列的 frontrear 相等时,队列为空;而环形队列的 headtail 相等时,既可能是队列为空,也可能是队列为满。为了区别这两种结果,通常规定环形队列最多只能有 MAX_SIZE - 1 个队列元素(MAX_SIZE 为环形队列的空间大小),当环形队列中只剩下一个空存储单元时,就认为队列就已经满了。因此,环形队列判为空的条件是 head = tail,而环形队列判满的条件是 head=(tail + 1) % MAX_SIZE

基于 C 语言实现环形队列

为了更好的利用环形队列,创建一个环形队列的库可以在任何 C 语言项目中方便地使用队列数据结构。环形队列是一种基于数组的队列实现,在使用数组构建的方式,在访问内存空间时会更加方便。

实现一个简单的环形队列库,需要现实包括基本的操作如初始化、入队、出队、检查队列是否为空和是否已满等等功能。基本变量就是队列本身、队头、队尾、队列容量(队列长度)等,为方便管理这些变量,可以用一个结构体包含起来:

c 复制代码
typedef struct {
    uint8_t *data;         // 存储数据的数组
    uint32_t head;         // 队头索引
    uint32_t tail;         // 队尾索引
    uint32_t capacity;     // 队列容量
    uint32_t count;        // 当前队列中的元素数量
} circular_queue_t;

[!NOTE]

我在这里多添加了一个 count,用于表示当前队列中的元素数量,这个变量不是必要的,只是可以更加简单的管理队列。

创建好队列后,第一件要做的事情就是初始化队列,所以环形队列库第一个功能就是初始化队列:

c 复制代码
void circular_queue_init(circular_queue_t *queue, uint8_t *data, uint32_t capacity)
{
    queue->data = data;
    queue->head = 0;
    queue->tail = 0;
    queue->capacity = capacity;
    queue->count = 0;
}

队列初始化完毕,就可以进行入队出队的操作了。首先是入队操作,如果没有元素在队列中,就谈不上出队了。入队成功返回 true,否则返回 false

c 复制代码
bool circular_queue_enqueue(circular_queue_t *queue, uint8_t value)
{
    if (circular_queue_is_full(queue))
        return false;  // 队列已满,无法入队

    queue->data[queue->tail] = value;
    queue->tail = (queue->tail + 1) % queue->capacity;
    queue->count++;
    return true;
}

出队操作与入队操作大致相同:

c 复制代码
bool circular_queue_dequeue(circular_queue_t *queue, uint8_t *value)
{
    if (circular_queue_is_empty(queue))
        return false;  // 队列为空,无法出队

    *value = queue->data[queue->head];
    queue->head = (queue->head + 1) % queue->capacity;
    queue->count--;
    return true;
}

正常来说,有以上三个函数就可以使用环形队列完成各种项目了。当然,也可以在这些功能的基础上添加其他的功能,这里不对其他功能一一阐述,下面是完整的 .h.c 文件:

环形队列库头文件(circular_queue.h)

c 复制代码
#ifndef __CIRCULAR_QUEUE_H__
#define __CIRCULAR_QUEUE_H__

#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

// 定义队列结构体
typedef struct {
    uint8_t *data;         // 存储数据的数组
    uint32_t head;         // 队头索引
    uint32_t tail;         // 队尾索引
    uint32_t capacity;     // 队列容量
    uint32_t count;        // 当前队列中的元素数量
} circular_queue_t;

// 初始化队列
void circular_queue_init(circular_queue_t *queue, uint8_t *data, uint32_t capacity);
// 入队操作
bool circular_queue_enqueue(circular_queue_t *queue, uint8_t value);
// 出队操作
bool circular_queue_dequeue(circular_queue_t *queue, uint8_t *value);
// 检查队列是否为空
bool circular_queue_is_empty(const circular_queue_t *queue);
// 检查队列是否已满
bool circular_queue_is_full(const circular_queue_t *queue);
// 获取队列大小
uint32_t circular_queue_size(const circular_queue_t *queue);
// 查看队头元素
bool circular_queue_peek_head(const circular_queue_t *queue, uint8_t *value);
// 查看队尾元素
bool circular_queue_peek_tail(const circular_queue_t *queue, uint8_t *value);
// 清空队列
void circular_queue_clear(circular_queue_t *queue);

#endif // __CIRCULAR_QUEUE_H__

环形队列库源文件(circular_queue.c)

c 复制代码
#include "circular_queue.h"

// 初始化队列
void circular_queue_init(circular_queue_t *queue, uint8_t *data, uint32_t capacity)
{
    queue->data = data;
    queue->head = 0;
    queue->tail = 0;
    queue->capacity = capacity;
    queue->count = 0;
}

// 入队操作
bool circular_queue_enqueue(circular_queue_t *queue, uint8_t value)
{
    if (circular_queue_is_full(queue))
        return false;  // 队列已满,无法入队

    queue->data[queue->tail] = value;
    queue->tail = (queue->tail + 1) % queue->capacity;
    queue->count++;
    return true;
}

// 出队操作
bool circular_queue_dequeue(circular_queue_t *queue, uint8_t *value)
{
    if (circular_queue_is_empty(queue))
        return false;  // 队列为空,无法出队

    *value = queue->data[queue->head];
    queue->head = (queue->head + 1) % queue->capacity;
    queue->count--;
    return true;
}

// 检查队列是否为空
bool circular_queue_is_empty(const circular_queue_t *queue)
{
    return queue->count == 0;
}

// 检查队列是否已满
bool circular_queue_is_full(const circular_queue_t *queue)
{
    return queue->count == queue->capacity;
}

// 获取队列大小
uint32_t circular_queue_size(const circular_queue_t *queue)
{
    return queue->count;
}

// 查看队头元素
bool circular_queue_peek_head(const circular_queue_t *queue, uint8_t *value)
{
    if (circular_queue_is_empty(queue))
        return false;  // 队列为空,无法查看队头元素

    *value = queue->data[queue->head];
    return true;
}

// 查看队尾元素
bool circular_queue_peek_tail(const circular_queue_t *queue, uint8_t *value)
{
    if (circular_queue_is_empty(queue))
        return false;  // 队列为空,无法查看队尾元素

    *value = queue->data[(queue->tail - 1 + queue->capacity) % queue->capacity];
    return true;
}

// 清空队列
void circular_queue_clear(circular_queue_t *queue)
{
    queue->head = 0;
    queue->tail = 0;
    queue->count = 0;
    memcpy(queue->data, 0, queue->capacity);
    queue->capacity = 0;
}

环形队列库的功能说明如下:

  • circular_queue_init:初始化队列,设置队列的数据指针、容量和其他属性。
  • circular_queue_enqueue :向队列中添加一个元素。如果队列已满,则返回 false
  • circular_queue_dequeue :从队列中移除并返回一个元素。如果队列为空,则返回 false
  • circular_queue_is_empty:检查队列是否为空。
  • circular_queue_is_full:检查队列是否已满。
  • circular_queue_size:返回当前队列中的元素数量。
  • circular_queue_peek_head :返回队头的元素,但不移除它。如果队列为空,则返回 false
  • circular_queue_peek_tail :返回队尾的元素,但不移除它。如果队列为空,则返回 false
  • circular_queue_clear:将队列清空,但不释放内存。

该库提供了一个基本的循环队列实现,当然也可以根据自己的需要进行扩展和修改。例如,当前的队列只支持 uint_8 数据类型的数组,可以改为 void 型数组,再配合枚举型变量,让队列类型可以兼容更多类型。

环形队列的应用场合

  1. 缓冲区管理

    • 输入/输出缓冲:在操作系统或嵌入式系统中,环形队列常用于处理输入/输出缓冲。例如,键盘输入、串口通信、网络数据包等。
    • 音频/视频流:在多媒体应用中,环形队列用于存储音频或视频数据块,确保平滑的数据流。
  2. 生产者-消费者问题

    • 多线程同步:在多线程或多进程环境中,环形队列是解决生产者-消费者问题的经典方法。生产者将数据放入队列,消费者从队列中取出数据,这样可以避免竞争条件和死锁。
    • 任务调度:在任务调度系统中,环形队列用于存储待处理的任务,确保任务按顺序执行。
  3. 日志记录

    • 事件日志:在系统监控和日志记录中,环形队列可以用来存储最近的日志条目。当队列满时,新的日志条目会覆盖最早的条目,从而保持最新的日志记录。
  4. 缓存机制

    • LRU缓存:在实现最近最少使用(Least Recently Used, LRU)缓存时,环形队列可以用来存储最近使用的项目。当缓存满时,最旧的项目会被移除。
  5. 消息队列

    • 异步通信:在分布式系统或消息传递系统中,环形队列可以作为消息队列,用于异步通信。生产者发送消息到队列,消费者从队列中接收消息。
  6. 实时系统

    • 实时数据处理:在实时系统中,环形队列用于存储传感器数据或其他实时数据,确保数据的及时处理。
  7. 游戏开发

    • 事件处理:在游戏中,环形队列可以用来存储玩家输入事件或其他游戏事件,确保事件按顺序处理。
  8. 硬件驱动程序

    • DMA传输:在直接内存访问(Direct Memory Access, DMA)传输中,环形队列用于存储传输的数据块,确保数据的连续性和高效性。
  9. 性能监控

    • 采样数据:在性能监控工具中,环形队列可以用来存储性能指标的采样数据,以便进行分析和报告。
  10. 算法实现

    • 滑动窗口算法:在一些算法中,如滑动窗口平均值计算或滑动窗口最大值/最小值计算,环形队列可以用来存储窗口内的数据。

  1. 队列存储区还没有满,但队列却发生了溢出,通常把这种现象称为 "假溢出"。 ↩︎
相关推荐
xiaoshiguang32 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡2 小时前
【C语言】判断回文
c语言·学习·算法
别NULL2 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇3 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
ZSYP-S4 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos4 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习4 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA4 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo4 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc4 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法