一、什么是队列
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),但容量浪费一个单元 |
下一篇我们讲链式队列,以及队列在广度优先搜索中的应用。
九、思考题
-
为什么循环队列要牺牲一个单元来区分队空和队满?如果不牺牲,还有其他办法吗?
-
假设
MAX_SIZE = 5,front = 3,rear = 2,队列中有几个元素?画出数组状态。 -
如果队列的
front和rear都指向同一个位置,如何判断队列是空还是满(不牺牲单元的情况下)? -
尝试实现一个支持动态扩容的循环队列。
欢迎在评论区讨论你的答案。