文章目录
数据结构番外:环形队列
环形队列的概念及结构
环形队列的概念
环形队列(Circular Queue) ,又称循环队列 ,是一种基于数组实现的、逻辑上首尾相接的线性数据结构。它通过让队头和队尾指针"绕回"数组起始位置,解决了普通顺序队列中假溢出 导致的空间浪费问题。
它的核心特征有以下几点:
- 先进先出(FIFO):与普通队列一致,元素只能从队尾入队、从队头出队。
- 逻辑成环 :物理上仍是一段连续数组,但下标通过取模运算
(i + 1) % N在尾部自动绕回 0,形成一个逻辑上的"环"。 - 空间可复用:出队释放的空间能被后续入队操作重新利用,不会像普通队列那样被永久浪费。
- 两指针 + 固定容量 :用
front和rear两个下标变量维护队头与队尾位置,容量在初始化时确定。
环形队列的结构
环形队列的物理结构是一个定长数组,加上两个指针:
c
#define MAXSIZE 8 /* 数组容量 */
typedef struct {
int data[MAXSIZE]; /* 存储元素的定长数组 */
int front; /* 队头下标:指向队头元素 */
int rear; /* 队尾下标:指向下一个可插入位置 */
} CircularQueue;
注意:这里的指针不是真的指针,因为这里是数组而数组访问最终会退化成指针访问,所以这里将两个下标称为指针。实际上在许多地方我们都会将上述这种称为指针。

环形队列的实现
我们通过上述对环形队列进行了一些了解,现在我们开始实现属于自己的环形队列,在实现之前我们先要想想如何实现:
-
环形队列如何判断是空还是满?
在环形队列里面判断空和满通常有两种方式:
- 舍弃一个空间来完成判空和满,空-> rear == front; 满-> rear + 1 == front;
- 计数器版本:定义一个count来计数,出队列--,入队列++,空-> count == 0; 满->count == MAXSIZE;
我们这里选择的是舍弃一个空间来完成判空和判满。
-
环形队列的出队列和入队列应该怎么处理?
现在考虑出队列和入队列该怎么处理了:
出队列:因为数组逻辑上带环,物理上就是一个普通数组。所以出队列时就要考虑不能越界,那么该如何操作呢?
(front + 1) % MAXSIZE; 这样就front永远不会超过MAXSIZE了也就不会越界访问。
入队列:同出队列不能越界的原因,(rear + 1) % MAXSIZE;
一下就是我们要实现的接口函数:
c
/* circular_queue.h */
#pragma once
#include <stdbool.h>
#define MAXSIZE 8
typedef struct {
int data[MAXSIZE];
int front;
int rear;
} CircularQueue;
void cq_init(CircularQueue *q);
bool cq_is_empty(const CircularQueue *q);
bool cq_is_full(const CircularQueue *q);
bool cq_enqueue(CircularQueue *q, int value);
bool cq_dequeue(CircularQueue *q, int *value);
bool cq_peek(const CircularQueue *q, int *value);
int cq_size(const CircularQueue *q);
void cq_print(const CircularQueue *q);
c
#include "circular_queue.h"
#include <stdio.h>
#include <stdbool.h>
void cq_init(CircularQueue *q)
{
q->front = 0;
q->rear = 0;
}
bool cq_is_empty(const CircularQueue *q)
{
return q->front == q->rear;
}
bool cq_is_full(const CircularQueue *q)
{
return (q->rear + 1) % MAXSIZE == q->front;
}
bool cq_enqueue(CircularQueue *q, int value)
{
if (cq_is_full(q)) {
return false; /* 队列满,入队失败 */
}
q->data[q->rear] = value;
q->rear = (q->rear + 1) % MAXSIZE;
return true;
}
bool cq_dequeue(CircularQueue *q, int *value)
{
if (cq_is_empty(q)) {
return false; /* 队列空,出队失败 */
}
*value = q->data[q->front];
q->front = (q->front + 1) % MAXSIZE;
return true;
}
bool cq_peek(const CircularQueue *q, int *value)
{
if (cq_is_empty(q)) {
return false;
}
*value = q->data[q->front];
return true;
}
int cq_size(const CircularQueue *q)
{
return (q->rear - q->front + MAXSIZE) % MAXSIZE;
}
void cq_print(const CircularQueue *q)
{
printf("Queue(front=%d, rear=%d, size=%d): ",
q->front, q->rear, cq_size(q));
if (cq_is_empty(q)) {
printf("(empty)\n");
return;
}
int i = q->front;
while (i != q->rear) {
printf("%d ", q->data[i]);
i = (i + 1) % MAXSIZE;
}
printf("\n");
}
计数器版本
计数器版本可选实现:
c
typedef struct {
int data[MAXSIZE];
int front;
int rear;
int count; /* 当前元素个数 */
} CircularQueue2;
bool cq2_is_empty(const CircularQueue2 *q) { return q->count == 0; }
bool cq2_is_full (const CircularQueue2 *q) { return q->count == MAXSIZE; }
bool cq2_enqueue(CircularQueue2 *q, int v)
{
if (cq2_is_full(q)) return false;
q->data[q->rear] = v;
q->rear = (q->rear + 1) % MAXSIZE;
q->count++;
return true;
}
bool cq2_dequeue(CircularQueue2 *q, int *v)
{
if (cq2_is_empty(q)) return false;
*v = q->data[q->front];
q->front = (q->front + 1) % MAXSIZE;
q->count--;
return true;
}
环形队列的优点和缺点
优点
| 优点 | 说明 |
|---|---|
| 空间复用,无假溢出 | 出队释放的空间能被后续入队重新利用,只要队列长度不超容量就能一直运行,不会像普通顺序队列那样出现"前面空着、后面却进不来"的假溢出。 |
| 入队 / 出队 O(1) | 只需移动 front 或 rear 下标并做一次取模,不涉及元素搬移,单次操作耗时恒定。 |
| 内存连续,缓存友好 | 底层是定长数组,元素在一块连续内存上,CPU 缓存命中率高,访问速度快。 |
| 无动态内存分配 | 容量在初始化时确定,运行期不 malloc / free,没有内存碎片和分配失败的风险,适合嵌入式、实时系统。 |
| 实现简单,易于封装 | 核心只有两个下标 + 一个数组,配上取模运算即可,接口清晰,容易写成头文件复用。 |
缺点
| 缺点 | 说明 |
|---|---|
| 容量固定,无法动态扩展 | 数组大小在编译期或初始化时确定,元素数量超出容量后只能覆盖旧数据或丢弃新数据。若想扩容必须重新申请内存并搬移元素。 |
| 牺牲一个存储单元(经典方案) | 用 (rear + 1) % N == front 判满时,实际可用容量是 N - 1,白白浪费一个位置。可用计数器版本解决,但要多维护一个变量。 |
| 空 / 满判断易混淆 | front == rear 既可能是空也可能是满,初学者容易写错判满条件,一旦漏掉取模就会在指针绕回时出现隐蔽 bug。 |
| 取模运算有开销 | 每次移动指针都要 % MAXSIZE,在没有硬件除法器的 MCU 上开销不可忽视;通常用"条件判断绕回"或"容量取 2 的幂 + 位与"来优化。 |
| 遍历 / 查找不如链表灵活 | 要遍历队列需从 front 走到 rear 并处理绕回,逻辑比链表遍历稍繁琐;插入 / 删除中间元素更是 O(n)。 |
| 线程安全需额外处理 | 多线程访问时,front、rear 的读写不是原子的,需要加锁或使用原子变量 + 内存屏障,否则会出现数据竞争。 |
总结 :环形队列用固定的连续内存 + 取模绕回 换来了 O(1) 的入队出队和高效的空间复用,代价是容量不可变 和判空判满的额外心智负担。它最适合"出入速率相近、数据量有上界"的场景;如果数据量波动大、需要频繁扩容,则应考虑动态数组队列或链式队列。
完