数据结构番外:环形队列

文章目录

数据结构番外:环形队列

环形队列的概念及结构

环形队列的概念

环形队列(Circular Queue) ,又称循环队列 ,是一种基于数组实现的、逻辑上首尾相接的线性数据结构。它通过让队头和队尾指针"绕回"数组起始位置,解决了普通顺序队列中假溢出 导致的空间浪费问题。

它的核心特征有以下几点:

  • 先进先出(FIFO):与普通队列一致,元素只能从队尾入队、从队头出队。
  • 逻辑成环 :物理上仍是一段连续数组,但下标通过取模运算(i + 1) % N 在尾部自动绕回 0,形成一个逻辑上的"环"。
  • 空间可复用:出队释放的空间能被后续入队操作重新利用,不会像普通队列那样被永久浪费。
  • 两指针 + 固定容量 :用front和rear两个下标变量维护队头与队尾位置,容量在初始化时确定。

环形队列的结构

环形队列的物理结构是一个定长数组,加上两个指针:

c 复制代码
#define MAXSIZE 8        /* 数组容量 */

typedef struct {
    int data[MAXSIZE];   /* 存储元素的定长数组 */
    int front;           /* 队头下标:指向队头元素 */
    int rear;            /* 队尾下标:指向下一个可插入位置 */
} CircularQueue;

注意:这里的指针不是真的指针,因为这里是数组而数组访问最终会退化成指针访问,所以这里将两个下标称为指针。实际上在许多地方我们都会将上述这种称为指针。

环形队列的实现

我们通过上述对环形队列进行了一些了解,现在我们开始实现属于自己的环形队列,在实现之前我们先要想想如何实现:

  1. 环形队列如何判断是空还是满?

    在环形队列里面判断空和满通常有两种方式:

    1. 舍弃一个空间来完成判空和满,空-> rear == front; 满-> rear + 1 == front;
    2. 计数器版本:定义一个count来计数,出队列--,入队列++,空-> count == 0; 满->count == MAXSIZE;

    我们这里选择的是舍弃一个空间来完成判空和判满。

  2. 环形队列的出队列和入队列应该怎么处理?

    现在考虑出队列和入队列该怎么处理了:
    出队列:因为数组逻辑上带环,物理上就是一个普通数组。所以出队列时就要考虑不能越界,那么该如何操作呢?
    (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) 的入队出队和高效的空间复用,代价是容量不可变 和判空判满的额外心智负担。它最适合"出入速率相近、数据量有上界"的场景;如果数据量波动大、需要频繁扩容,则应考虑动态数组队列或链式队列。

完

相关推荐
50万马克的面包2 小时前
顺序表和链表_精华知识点_CSDN博客
数据结构·链表
All for pursuit.2 小时前
【回溯-7】494.目标和
数据结构·c++·算法·leetcode
50万马克的面包2 小时前
数据结构01-数据结构概念与复杂度-博客稿
数据结构
Logic1012 小时前
C语言/数据结构贪心算法题解:字符串趋同——逐位统计最高频率求最小修改代价
c语言·数据结构·贪心算法·字符串·时间复杂度·频率统计·算法题
91刘仁德3 小时前
传输层协议深度拆解:端口寻址、UDP 报文与 TCP 可靠性机制全解析
linux·网络·c++·网络协议·tcp/ip·udp·c
红豆诗人4 小时前
C++二叉搜索树:查找、插入和删除为什么这样写
数据结构·c++·算法
H.莓飛4 小时前
【数据结构】堆_OJ题
linux·数据结构·算法
7yewh13 小时前
SLAM 三维空间刚体运动(2)
数据结构·人工智能·机器人·嵌入式·slam
leikooo13 小时前
用餐厅和盘子理解栈和堆
java·数据结构·链表·rust