文章目录
- 什么是环形队列?
- 基于 C 语言实现环形队列
- 环形队列的应用场合
在计算机科学中,数据结构是组织和存储数据的方式,它对于高效的算法设计至关重要。队列是一种常见的数据结构,遵循 FIFO(先进先出,First-In-First-Out)的原则。然而,在实际应用中,传统的线性队列可能会遇到一些问题,例如当队列满时无法再插入新元素,即使队列头部有空闲空间。为了解决这些问题, 环形队列(也称为 循环队列)应运而生。
环形队列通过将数组的末端与开头连接起来形成一个循环结构,从而更有效地利用内存空间。这种数据结构在许多应用场景中都非常有用,包括缓冲区管理、生产者-消费者问题、日志记录、消息队列等。本文是我学习环形队列时所作笔记的总结,并使用 C 语言实现一个高效且功能丰富的环形队列,以及探讨其在实际项目中的应用。
什么是环形队列?
为充分利用向量空间,克服 “ 假溢出1” 现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
——引用至《循环队列_百度百科》
要理解环形队列,可以先从单个变量的读写开始。
以串口收发数据为例,假设某些外设通过串口向主机发送 1 byte 的数据,且每秒发送一次,那么这 1 byte 的数据会先存在主机的接收缓冲存储区中。假设主机的串口接收缓冲存储区大小也刚好为 1 byte,一秒的间隔时间足够主机将缓冲存储区的数据及时读走并处理。
但是,如果发送的数据不止 1 byte,而是多个字节的情况,那么可能就会存在某个数据还没被读取,接收缓冲存储区就被下一个数据覆盖的情况。这时可以用中断的方式来处理这种情况,只要一数据存入接收缓冲存储区,就立刻触发中断把数据取走,存入其他存储区。不过,这样也有一个问题,如果上一个数据还没处理完毕,也会被中断程序覆盖为下一个数据,同样存在数据丢失的情况。
解决这个问题的办法也很简单,用一个相对较大的存储区,就可以存放更多的还没来得及处理的数据。根据先收到的数据先处理的原则,就可以用顺序队列作为这个存储区。
顺序队列有队头和队尾,而且队头和队尾的位置是可以变化的,所以需要设置两个指针 front
和 rear
分别指示队头元素和队尾元素在向量空间中的位置,它们的初值在队列初始化时均应设置为 0。
新元素出入队列的动作称为入队,入队时将新元素插入 rear
所指的位置,然后将 rear
加 1,即指向下一个没有数据的空间。元素从队列被读出的动作成为出队,出队时,读取 front
所指的元素后将元素删去,然后将 front
加 1 并,即指向下一个待处理数据的空间。
使用顺序队列后,即使来不急处理的数据,也能被逐个存储起来等待处理。但是,仍然存在一些弊端,根据顺序队列的特点,如果队列有元素,队头指针始终指向队头元素,队尾指针始终指向队尾元素的下一位置。所以当头尾指针相等时,队列为空。那么就会存在一种这样的情况(如下图所示),队列的空间并不是无限大的,当 rear
等于队列总长时,说明队列已经 “满” 了。此时若再执行入队操作,便会出现队满 “溢出”。然而,由于在此之前可能也执行了若干次出队操作.因而队列的前面部分可能还有很多闲置的元素空间,即这种溢出并非是真的没有可用的存储空间,故称这种溢出现象为 “假溢出”。
如果还有数据要入队但又无法入队,势必会造成数据丢失的情况,因此要一定解决队列这种 “假溢出” 的问题,否则使用队列就没有太多价值。
队列的 “假溢出” 现象,是因为又很多空间没有利用到,那么克服这种现象的办法就是:将队列空间想象为一个首尾相接的圆环,并称这种队列为环形队列,也叫循环队列。
循环队列就是将队列存储空间的最后一个位置绕到第一个位置,形成逻辑上的环状空间,供队列循环使用。在循环队列结构中,当存储空间的最后一个位置已被使用而再要进入队运算时,只需要存储空间的第一个位置空闲,便可将元素加入到第一个位置,即将存储空间的第一个位置作为队尾,可以更简单防止 “假溢出” 的发生。
更加形象的表示方法如下图所示,环形队列同样需要两个指针分别表示队头(head
指针)和队尾( tail
指针),与顺序队列的队头和队尾大致相同,入队时由队尾 tail
指针写入,并向后偏移;出队时由队头 head
指针取出后删除,并向后偏移。
环形队列有一种特殊情况与顺序队列不同,顺序队列的 front
和 rear
相等时,队列为空;而环形队列的 head
和 tail
相等时,既可能是队列为空,也可能是队列为满。为了区别这两种结果,通常规定环形队列最多只能有 MAX_SIZE - 1
个队列元素(MAX_SIZE
为环形队列的空间大小),当环形队列中只剩下一个空存储单元时,就认为队列就已经满了。因此,环形队列判为空的条件是 head = tail
,而环形队列判满的条件是 head=(tail + 1) % MAX_SIZE
。
基于 C 语言实现环形队列
为了更好的利用环形队列,创建一个环形队列的库可以在任何 C 语言项目中方便地使用队列数据结构。环形队列是一种基于数组的队列实现,在使用数组构建的方式,在访问内存空间时会更加方便。
实现一个简单的环形队列库,需要现实包括基本的操作如初始化、入队、出队、检查队列是否为空和是否已满等等功能。基本变量就是队列本身、队头、队尾、队列容量(队列长度)等,为方便管理这些变量,可以用一个结构体包含起来:
typedef struct {
uint8_t *data; // 存储数据的数组
uint32_t head; // 队头索引
uint32_t tail; // 队尾索引
uint32_t capacity; // 队列容量
uint32_t count; // 当前队列中的元素数量
} circular_queue_t;
[!NOTE]
我在这里多添加了一个
count
,用于表示当前队列中的元素数量,这个变量不是必要的,只是可以更加简单的管理队列。
创建好队列后,第一件要做的事情就是初始化队列,所以环形队列库第一个功能就是初始化队列:
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
:
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;
}
正常来说,有以上三个函数就可以使用环形队列完成各种项目了。当然,也可以在这些功能的基础上添加其他的功能,这里不对其他功能一一阐述,下面是完整的 .h
和 .c
文件:
环形队列库头文件(circular_queue.h)
#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)
#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
型数组,再配合枚举型变量,让队列类型可以兼容更多类型。
环形队列的应用场合
-
缓冲区管理
- 输入/输出缓冲:在操作系统或嵌入式系统中,环形队列常用于处理输入/输出缓冲。例如,键盘输入、串口通信、网络数据包等。
- 音频/视频流:在多媒体应用中,环形队列用于存储音频或视频数据块,确保平滑的数据流。
-
生产者-消费者问题
- 多线程同步:在多线程或多进程环境中,环形队列是解决生产者-消费者问题的经典方法。生产者将数据放入队列,消费者从队列中取出数据,这样可以避免竞争条件和死锁。
- 任务调度:在任务调度系统中,环形队列用于存储待处理的任务,确保任务按顺序执行。
-
日志记录
- 事件日志:在系统监控和日志记录中,环形队列可以用来存储最近的日志条目。当队列满时,新的日志条目会覆盖最早的条目,从而保持最新的日志记录。
-
缓存机制
- LRU缓存:在实现最近最少使用(Least Recently Used, LRU)缓存时,环形队列可以用来存储最近使用的项目。当缓存满时,最旧的项目会被移除。
-
消息队列
- 异步通信:在分布式系统或消息传递系统中,环形队列可以作为消息队列,用于异步通信。生产者发送消息到队列,消费者从队列中接收消息。
-
实时系统
- 实时数据处理:在实时系统中,环形队列用于存储传感器数据或其他实时数据,确保数据的及时处理。
-
游戏开发
- 事件处理:在游戏中,环形队列可以用来存储玩家输入事件或其他游戏事件,确保事件按顺序处理。
-
硬件驱动程序
- DMA传输:在直接内存访问(Direct Memory Access, DMA)传输中,环形队列用于存储传输的数据块,确保数据的连续性和高效性。
-
性能监控
- 采样数据:在性能监控工具中,环形队列可以用来存储性能指标的采样数据,以便进行分析和报告。
-
算法实现
- 滑动窗口算法:在一些算法中,如滑动窗口平均值计算或滑动窗口最大值/最小值计算,环形队列可以用来存储窗口内的数据。
队列存储区还没有满,但队列却发生了溢出,通常把这种现象称为 “假溢出”。 ↩︎