【数据结构与算法】第14篇:队列(一):循环队列(顺序存储

一、什么是队列

1.1 队列的定义

队列(Queue)是一种操作受限的线性表,只允许在一端插入(队尾),在另一端删除(队头)。

核心特性:先进先出(FIFO,First In First Out)

text

复制代码
出队 ← [队头] [数据1] [数据2] [数据3] [队尾] ← 入队

现实中的例子:排队买票、打印机任务队列。

1.2 队列的基本操作

操作 说明
enqueue 入队,在队尾插入元素
dequeue 出队,删除队头元素并返回
front 获取队头元素,不删除
isEmpty 判断队列是否为空
isFull 判断队列是否已满

二、顺序队列的假溢出问题

2.1 简单的顺序队列

用数组实现队列,需要两个指针:

  • front:指向队头

  • rear:指向队尾(通常指向最后一个元素的下一个位置)

c

复制代码
// 初始状态:front = rear = 0
// 入队:data[rear] = value; rear++
// 出队:value = data[front]; front++

2.2 假溢出

假设数组容量为5,经过几次入队和出队:

text

复制代码
初始: front=0, rear=0
入队1,2,3 → front=0, rear=3
出队1,2   → front=2, rear=3
此时队列有1个元素,但rear=3
再入队4,5 → front=2, rear=5(数组越界!)

问题:数组前面还有空位(下标0、1),但rear已经指向数组末尾,无法继续插入。这就是假溢出

2.3 循环队列的解决方案

循环队列把数组想象成环状:当rear到达数组末尾时,下一个位置回到开头。

关键:取模运算 %

  • rear = (rear + 1) % capacity

  • front = (front + 1) % capacity

这样数组空间可以循环使用,解决假溢出问题。


三、循环队列的实现

3.1 结构定义

c

复制代码
#define MAX_SIZE 5  // 队列容量

typedef struct {
    int data[MAX_SIZE];
    int front;  // 队头指针
    int rear;   // 队尾指针
} CircleQueue;

3.2 初始化

c

复制代码
void initQueue(CircleQueue *queue) {
    queue->front = 0;
    queue->rear = 0;
}

3.3 判断队空

front == rear 时,队列为空。

c

复制代码
int isEmpty(CircleQueue *queue) {
    return queue->front == queue->rear;
}

3.4 判断队满(关键!)

front == rear 已经被用来表示队空了,那队满怎么表示?

方案一:牺牲一个单元(常用)

(rear + 1) % MAX_SIZE == front 表示队满。也就是说,始终留一个空位不用。

此时队列最多能存 MAX_SIZE - 1 个元素。

c

复制代码
int isFull(CircleQueue *queue) {
    return (queue->rear + 1) % MAX_SIZE == queue->front;
}

方案二:增加一个size变量

size 记录元素个数,size == MAX_SIZE 表示队满。这个方案更直观,但多占一个变量。

c

复制代码
typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
    int size;  // 当前元素个数
} CircleQueue;

int isFull(CircleQueue *queue) {
    return queue->size == MAX_SIZE;
}

本专栏采用方案一(牺牲一个单元),这是最常见的写法。

3.5 入队(enqueue)

c

复制代码
int enqueue(CircleQueue *queue, int value) {
    if (isFull(queue)) {
        printf("队列已满,无法入队\n");
        return -1;
    }
    queue->data[queue->rear] = value;
    queue->rear = (queue->rear + 1) % MAX_SIZE;
    return 0;
}

3.6 出队(dequeue)

c

复制代码
int dequeue(CircleQueue *queue, int *value) {
    if (isEmpty(queue)) {
        printf("队列为空,无法出队\n");
        return -1;
    }
    *value = queue->data[queue->front];
    queue->front = (queue->front + 1) % MAX_SIZE;
    return 0;
}

3.7 获取队头元素

c

复制代码
int getFront(CircleQueue *queue, int *value) {
    if (isEmpty(queue)) {
        printf("队列为空\n");
        return -1;
    }
    *value = queue->data[queue->front];
    return 0;
}

3.8 获取队列长度

c

复制代码
int getSize(CircleQueue *queue) {
    // 两种情况
    if (queue->rear >= queue->front) {
        return queue->rear - queue->front;
    } else {
        return MAX_SIZE - (queue->front - queue->rear);
    }
    // 等价写法:(rear - front + MAX_SIZE) % MAX_SIZE
}

// 更简洁的写法
int getSize(CircleQueue *queue) {
    return (queue->rear - queue->front + MAX_SIZE) % MAX_SIZE;
}

四、完整代码演示

c

复制代码
#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 5

typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
} CircleQueue;

void initQueue(CircleQueue *queue) {
    queue->front = 0;
    queue->rear = 0;
}

int isEmpty(CircleQueue *queue) {
    return queue->front == queue->rear;
}

int isFull(CircleQueue *queue) {
    return (queue->rear + 1) % MAX_SIZE == queue->front;
}

int enqueue(CircleQueue *queue, int value) {
    if (isFull(queue)) {
        printf("队列已满,%d 入队失败\n", value);
        return -1;
    }
    queue->data[queue->rear] = value;
    queue->rear = (queue->rear + 1) % MAX_SIZE;
    printf("%d 入队成功\n", value);
    return 0;
}

int dequeue(CircleQueue *queue, int *value) {
    if (isEmpty(queue)) {
        printf("队列为空,无法出队\n");
        return -1;
    }
    *value = queue->data[queue->front];
    queue->front = (queue->front + 1) % MAX_SIZE;
    return 0;
}

int getFront(CircleQueue *queue, int *value) {
    if (isEmpty(queue)) {
        printf("队列为空\n");
        return -1;
    }
    *value = queue->data[queue->front];
    return 0;
}

int getSize(CircleQueue *queue) {
    return (queue->rear - queue->front + MAX_SIZE) % MAX_SIZE;
}

void printQueue(CircleQueue *queue) {
    if (isEmpty(queue)) {
        printf("队列为空\n");
        return;
    }
    printf("队头 -> ");
    int i = queue->front;
    while (i != queue->rear) {
        printf("%d ", queue->data[i]);
        i = (i + 1) % MAX_SIZE;
    }
    printf("<- 队尾 (size=%d, max=%d)\n", getSize(queue), MAX_SIZE - 1);
}

int main() {
    CircleQueue queue;
    initQueue(&queue);
    
    printf("=== 入队测试 ===\n");
    enqueue(&queue, 10);
    enqueue(&queue, 20);
    enqueue(&queue, 30);
    enqueue(&queue, 40);
    printQueue(&queue);
    
    // 再入队一个,应该失败(容量MAX_SIZE-1=4)
    enqueue(&queue, 50);
    printQueue(&queue);
    
    printf("\n=== 出队测试 ===\n");
    int val;
    dequeue(&queue, &val);
    printf("出队: %d\n", val);
    dequeue(&queue, &val);
    printf("出队: %d\n", val);
    printQueue(&queue);
    
复制代码
 printf("\n=== 继续入队(验证循环) ===\n");
    enqueue(&queue, 50);
    enqueue(&queue, 60);
    printQueue(&queue);
    
    printf("\n=== 获取队头 ===\n");
    getFront(&queue, &val);
    printf("队头元素: %d\n", val);
    printf("当前队列大小: %d\n", getSize(&queue));
    
    printf("\n=== 清空队列 ===\n");
    while (!isEmpty(&queue)) {
        dequeue(&queue, &val);
        printf("出队: %d\n", val);
    }
    printQueue(&queue);
    
    return 0;
}

运行结果:

text

复制代码
=== 入队测试 ===
10 入队成功
20 入队成功
30 入队成功
40 入队成功
队头 -> 10 20 30 40 <- 队尾 (size=4, max=4)
队列已满,50 入队失败
队头 -> 10 20 30 40 <- 队尾 (size=4, max=4)

=== 出队测试 ===
出队: 10
出队: 20
队头 -> 30 40 <- 队尾 (size=2, max=4)

=== 继续入队(验证循环) ===
50 入队成功
60 入队成功
队头 -> 30 40 50 60 <- 队尾 (size=4, max=4)

=== 获取队头 ===
队头元素: 30
当前队列大小: 4

=== 清空队列 ===
出队: 30
出队: 40
出队: 50
出队: 60
队列为空

五、队空和队满的区分

这是循环队列最核心的知识点,单独总结一下:

方案一:牺牲一个单元(本文采用)

条件 含义
front == rear 队空
(rear + 1) % MAX_SIZE == front 队满

特点

  • 实现简单,不需要额外变量

  • 最大容量为 MAX_SIZE - 1

  • 这是最常见的实现方式

方案二:增加size变量

c

复制代码
typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
    int size;
} CircleQueue;

// 队空:size == 0
// 队满:size == MAX_SIZE

特点

  • 更直观,不需要牺牲空间

  • 多维护一个变量

方案三:增加tag标志位

tag 标记上一次操作是入队还是出队,也能区分队空队满。但用得较少。


六、取模运算的作用

取模运算 % 是循环队列的核心:

c

复制代码
// 指针移动
rear = (rear + 1) % MAX_SIZE;
front = (front + 1) % MAX_SIZE;

// 计算长度
size = (rear - front + MAX_SIZE) % MAX_SIZE;

// 判断队满
isFull = (rear + 1) % MAX_SIZE == front;

为什么能循环?

  • rear = MAX_SIZE - 1 时,(rear + 1) % MAX_SIZE = 0,回到开头

  • 同理,front 也能回到开头


七、复杂度分析

操作 时间复杂度 说明
enqueue O(1) 直接修改rear指针
dequeue O(1) 直接修改front指针
front O(1) 直接访问数组
isEmpty/isFull O(1) 比较指针

八、小结

这一篇我们实现了循环队列:

要点 说明
假溢出 数组前面有空位但rear已到末尾
循环队列 用取模运算实现循环,解决假溢出
队空条件 front == rear
队满条件 (rear + 1) % MAX_SIZE == front
长度计算 (rear - front + MAX_SIZE) % MAX_SIZE
优缺点 操作O(1),但容量浪费一个单元

下一篇我们讲链式队列,以及队列在广度优先搜索中的应用。


九、思考题

  1. 为什么循环队列要牺牲一个单元来区分队空和队满?如果不牺牲,还有其他办法吗?

  2. 假设 MAX_SIZE = 5front = 3rear = 2,队列中有几个元素?画出数组状态。

  3. 如果队列的 frontrear 都指向同一个位置,如何判断队列是空还是满(不牺牲单元的情况下)?

  4. 尝试实现一个支持动态扩容的循环队列。

欢迎在评论区讨论你的答案。

相关推荐
NAGNIP2 小时前
一文搞懂CNN经典架构-ResNet!
算法·面试
Java_小白呀2 小时前
考研408数据结构(持续更新中...)
数据结构·考研
Frostnova丶2 小时前
(11)LeetCode 239. 滑动窗口最大值
数据结构·算法·leetcode
IT从业者张某某2 小时前
基于EGE19.01完成恐龙跳跃游戏-V00-C++使用EGE19.01这个轮子
c++·游戏
爱编码的小八嘎2 小时前
C语言完美演绎6-9
c语言
GoCoding2 小时前
YOLO-Master 与 YOLO26 开始
算法
VALENIAN瓦伦尼安教学设备2 小时前
设备对中不良的危害
数据库·嵌入式硬件·算法
weixin_649555672 小时前
C语言程序设计第四版(何钦铭、颜晖)第十一章指针进阶之奇数值结点链表
c语言·开发语言·链表
书到用时方恨少!2 小时前
Python os 模块使用指南:系统交互的瑞士军刀
开发语言·python