| 上一篇 | 下一篇 |
|---|---|
| 栈 |
目 录
- 队列(Queue)
队列(Queue)
核心思想:先进先出(FIFO, First In First Out) ------ 排队买票,先来先服务!
1)队列的定义与特点
-
定义: 队列是一种 先进先出(FIFO, First In First Out)的线性表 ,只允许在一端插入(入队,Enqueue) ,在另一端删除(出队,Dequeue),允许插入的一端称为队尾(rear),允许删除的一端则称为队头(front)。
-
特点:
- 入队:从 队尾 添加元素
- 出队:从 队头 移除元素
- 不允许中间插入或随机访问
-
描述:
入队 → [ C ][ B ][ A ] → 出队 ↑ ↑ 队尾(rear) 队头(front) -
举例:
假设有队列 q = ( a 1 , a 2 , . . , a n ) q=(a_1,a_2,..,a_n) q=(a1,a2,..,an) ,那么 a 1 a_1 a1 就是队头元素, a n a_n an 就是队尾元素。队列中的元素是按照 a 1 , a 2 , . . , a n a_1,a_2,..,a_n a1,a2,..,an 的顺序进入的,退出队列也只能按照这个次序依次退出,也就是说,只有在 a 1 , a 2 , . . . , a n − 1 a_1,a_2,...,a_{n-1} a1,a2,...,an−1 都离开队列之后, a n a_n an 才能退出队列。
2)队列的两种实现方式
| 对比项 | 顺序队列(数组) | 链队列(链表) |
|---|---|---|
| 内存分配 | 连续、需预设容量 | 动态、按需分配 |
| 空间效率 | 可能浪费(假溢出) | 无浪费 |
| 时间复杂度 | Enqueue/Dequeue: O(1) | Enqueue/Dequeue: O(1) |
| 实现难度 | 中等(需处理循环) | 简单(天然动态) |
3)普通顺序队列(存在"假溢出"问题)
缺点:空间利用率低,可能提前报"满"
3.1)基础结构设计
c
#define MAX_SIZE 100 // 固定容量
typedef int QDataType;
typedef struct SeqQueue {
QDataType data[MAX_SIZE]; // 定义一个大数组
int front; // 队头下标(指向第一个元素)
int rear; // 队尾下标(指向最后一个元素的下一个位置)
} SeqQueue;
3.2)假溢出问题
在使用数组实现的普通顺序队列中,会出现一种看似"队列已满",但实际上数组前面还有空闲空间的情况。这种"满"并不是真正的空间耗尽,而是由于队列的线性结构和操作方式导致的逻辑浪费 ,因此称为 "假溢出"(False Overflow)。示例如下:
初始:front = 0, rear = 0 → 空队
入队 A,B,C:front=0, rear=3 → [A][B][C][ ][ ]
出队 A,B:front=2, rear=3 → [ ][ ][C][ ][ ]
再入队 D,E,F:front=2, rear=6 → [ ][ ][C][D][E][F]
此时 rear == MAX_SIZE,无法再入队!
但前面还有 2 个空位 → 假溢出
简单说就是:队头的数据后来出队了,但是由于只能从队尾入队,所以空出来的队头就没法再被利用到。
3.3)核心操作(有缺陷)
-
初始化
cvoid QueueInit(SeqQueue* q) { q->front = q->rear = 0; } -
入队(不检查假溢出)
cbool QueuePush(SeqQueue* q, QDataType x) { if (q->rear >= MAX_SIZE) { return false; // 队满(可能是假溢出!) } q->data[q->rear++] = x; return true; } -
判断队列是否为空
cif (q->front == q->rear) { return false; // 队空 }初始化时,front 和 rear 的值相等;后续入队出队时,当所有元素都出队了时,front 又会遇到 rear。所以可以通过看这两个的值是不是相等来判断队列是否为空。
-
判断队列是否已满
cif (q->rear == MAX_SIZE) { return false; // 已满 } -
出队
c/* 出队:成功返回 true 并将元素存入 *out_val;失败返回 false */ bool QueuePop(SeqQueue* q, QDataType* out_val) { if (q == NULL || out_val == NULL) { return false; // 防御性检查 } // 判断队列是否为空 if (q->front == q->rear) { return false; // 队空,无法出队 } // 取出队头元素 *out_val = q->data[q->front]; q->front++; // 队头指针后移 return true; }
4)循环队列(解决假溢出)
有几个重要的计算公式
改进思路:
- 将数组视为环形结构
- 使用 模运算
% MAX_SIZE实现"首尾相连",队尾用(rear + 1) % MAX_SIZE表示(队尾永远指向 rear 后面一个)。
判空与判满,为区分"空"和"满",会牺牲一个存储单元 (容量 = MAX_SIZE - 1,最后空一个会显示已满):
| 状态 | 条件 |
|---|---|
| 队空 | front == rear |
| 队满 | (rear + 1) % MAX_SIZE == front(假设 MAX_SIZE=8,front=0,rear=7,则为满) |
图解:

结构(同普通顺序队列):
c
#define MAX_SIZE 100
typedef struct CircularQueue {
QDataType data[MAX_SIZE];
int front; // 队头下标
int rear; // 队尾下标(指向下一个空位)
} CircularQueue;
4.1)核心操作(正确实现)
初始化:
c
void QueueInit(CircularQueue* q) {
q->front = q->rear = 0;
}
入队:
c
bool QueuePush(CircularQueue* q, QDataType x) {
if ((q->rear + 1) % MAX_SIZE == q->front) {
return false; // 队满
}
q->data[q->rear] = x;
q->rear = (q->rear + 1) % MAX_SIZE; // !!!!!!!!!!!!
return true;
}
出队:
c
bool QueuePop(CircularQueue* q) {
if (q->front == q->rear) {
return false; // 队空
}
q->front = (q->front + 1) % MAX_SIZE; // !!!!!!!!!!!!
return true;
}
获取队头/队尾:
c
QDataType QueueFront(CircularQueue* q) {
return q->data[q->front];
}
QDataType QueueBack(CircularQueue* q) {
// rear 指向下一个空位,所以队尾是 (rear - 1 + MAX_SIZE) % MAX_SIZE
return q->data[(q->rear - 1 + MAX_SIZE) % MAX_SIZE]; // !!!!!!!!!!!!
}
获取大小:
c
int QueueSize(CircularQueue* q) {
return (q->rear - q->front + MAX_SIZE) % MAX_SIZE; // !!!!!!!!!!!!
}
5)链队列(动态实现,推荐)
链队列的头和尾是自己定的:可以用首元节点充当队头,用尾节点当队尾;也可以反过来。
结构设计:
c
/* 定义队列节点 */
typedef struct QueueNode {
QDataType data; // 数据域
struct QueueNode* next; // 指针域
} QueueNode;
/* 定义队列链表 */
typedef struct LinkQueue {
QueueNode* head; // 队头(出队端)
QueueNode* tail; // 队尾(入队端)
int size;
} LinkQueue;
💡 保留
tail指针使入队操作 O(1)
具体的常用操作见下面的完整代码实现。
6)C语言完整代码实现(循环队列/链队列)
6.1)循环队列
circular_queue.h
c
#ifndef CIRCULAR_QUEUE_H
#define CIRCULAR_QUEUE_H
#include <stdbool.h>
#define MAX_QSIZE 100 // 最大容量为 MAX_QSIZE - 1
typedef int QDataType;
typedef struct CircularQueue {
QDataType data[MAX_QSIZE];
int front;
int rear;
} CircularQueue;
void QueueInit(CircularQueue* q);
void QueueDestroy(CircularQueue* q); // 循环队列无需 destroy,留空
bool QueuePush(CircularQueue* q, QDataType x);
bool QueuePop(CircularQueue* q);
QDataType QueueFront(CircularQueue* q);
QDataType QueueBack(CircularQueue* q);
bool QueueEmpty(CircularQueue* q);
int QueueSize(CircularQueue* q);
#endif
circular_queue.c
c
#include "circular_queue.h"
void QueueInit(CircularQueue* q) {
q->front = q->rear = 0;
}
void QueueDestroy(CircularQueue* q) {
// 顺序结构,无需释放
}
bool QueuePush(CircularQueue* q, QDataType x) {
if ((q->rear + 1) % MAX_QSIZE == q->front) {
return false; // 队满
}
q->data[q->rear] = x;
q->rear = (q->rear + 1) % MAX_QSIZE;
return true;
}
bool QueuePop(CircularQueue* q) {
if (q->front == q->rear) {
return false; // 队空
}
q->front = (q->front + 1) % MAX_QSIZE;
return true;
}
QDataType QueueFront(CircularQueue* q) {
return q->data[q->front];
}
QDataType QueueBack(CircularQueue* q) {
return q->data[(q->rear - 1 + MAX_QSIZE) % MAX_QSIZE];
}
bool QueueEmpty(CircularQueue* q) {
return q->front == q->rear;
}
int QueueSize(CircularQueue* q) {
return (q->rear - q->front + MAX_QSIZE) % MAX_QSIZE;
}
6.2)链队列
link_queue.h
c
#ifndef LINK_QUEUE_H
#define LINK_QUEUE_H
#include <stdbool.h>
typedef int QDataType;
typedef struct QueueNode {
QDataType data;
struct QueueNode* next;
} QueueNode;
typedef struct LinkQueue {
QueueNode* head;
QueueNode* tail;
int size;
} LinkQueue;
void QueueInit(LinkQueue* q);
void QueueDestroy(LinkQueue* q);
void QueuePush(LinkQueue* q, QDataType x);
void QueuePop(LinkQueue* q);
QDataType QueueFront(LinkQueue* q);
QDataType QueueBack(LinkQueue* q);
bool QueueEmpty(LinkQueue* q);
int QueueSize(LinkQueue* q);
#endif
link_queue.c
c
#include "link_queue.h"
#include <stdlib.h>
#include <stdio.h>
/**
* @brief 初始化一个空的链式队列(不包含哨兵头节点)
* @param q 指向待初始化队列的指针(必须非 NULL)
*
* 将队列的头指针(head)和尾指针(tail)都置为 NULL,
* 表示队列中没有任何节点;同时将 size 置为 0。
*/
void QueueInit(LinkQueue* q) {
q->head = q->tail = NULL; // 头尾指针均为空,表示空队列
q->size = 0; // 队列元素个数初始化为 0
}
/**
* @brief 销毁队列,释放所有节点占用的内存
* @param q 指向待销毁队列的指针
*
* 通过不断调用 QueuePop() 逐个删除队列中的节点,
* 直到队列变空。这样可以确保所有 malloc 分配的内存都被 free。
*/
void QueueDestroy(LinkQueue* q) {
// 循环直到队列为空
while (!QueueEmpty(q)) {
QueuePop(q); // 每次弹出队头节点并释放其内存
}
// 此时 head 和 tail 已为 NULL,size 为 0,队列完全清空
}
/**
* @brief 入队操作:将元素 x 添加到队列尾部
* @param q 指向队列的指针
* @param x 要入队的数据(类型为 QDataType)
*
* 创建一个新节点,将其链接到当前 tail 节点之后,
* 并更新 tail 指针。若队列原本为空,则 head 和 tail 都指向新节点。
*/
void QueuePush(LinkQueue* q, QDataType x) {
// 动态分配一个新队列节点
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
// 如果内存分配失败,程序异常终止(实际项目中可改为返回错误码)
if (!newNode) exit(-1);
newNode->data = x; // 存储数据
newNode->next = NULL; // 新节点是队尾,next 指向 NULL
// 判断队列是否为空
if (q->tail == NULL) {
// 队列为空:新节点既是头也是尾
q->head = q->tail = newNode;
} else {
// 队列非空:将新节点接在原 tail 之后,并更新 tail
q->tail->next = newNode;
q->tail = newNode;
}
q->size++; // 队列元素数量加 1
}
/**
* @brief 出队操作:移除并释放队头元素
* @param q 指向队列的指针
*
* 注意:此实现不返回被移除的值,仅完成删除。
* 若队列为空,打印错误信息并终止程序。
*/
void QueuePop(LinkQueue* q) {
// 检查队列是否为空
if (QueueEmpty(q)) {
fprintf(stderr, "Error: Pop from empty queue!\n");
exit(-1); // 异常退出,避免非法访问
}
// 保存待删除的队头节点
QueueNode* del = q->head;
// 将 head 指向下一个节点(可能变为 NULL)
q->head = q->head->next;
// 特殊情况:如果删除后队列变空,需同步更新 tail 为 NULL
if (q->head == NULL) {
q->tail = NULL;
}
// 释放原队头节点的内存
free(del);
q->size--; // 队列元素数量减 1
}
/**
* @brief 获取队头元素的值(不删除)
* @param q 指向队列的指针
* @return 队头元素的数据
*
* 若队列为空,直接终止程序(无返回错误机制)。
*/
QDataType QueueFront(LinkQueue* q) {
if (QueueEmpty(q)) exit(-1); // 空队列不能取队头
return q->head->data; // 返回头节点存储的数据
}
/**
* @brief 获取队尾元素的值(不删除)
* @param q 指向队列的指针
* @return 队尾元素的数据
*
* 由于维护了 tail 指针,可 O(1) 直接访问队尾。
*/
QDataType QueueBack(LinkQueue* q) {
if (QueueEmpty(q)) exit(-1); // 空队列不能取队尾
return q->tail->data; // 返回尾节点存储的数据
}
/**
* @brief 判断队列是否为空
* @param q 指向队列的指针
* @return true 表示空,false 表示非空
*
* 以 head 是否为 NULL 作为判断依据(也可用 tail 或 size)。
*/
bool QueueEmpty(LinkQueue* q) {
return q->head == NULL;
}
/**
* @brief 获取队列当前元素个数
* @param q 指向队列的指针
* @return 队列中元素的数量
*
* 由于维护了 size 成员,可 O(1) 直接返回,无需遍历。
*/
int QueueSize(LinkQueue* q) {
return q->size;
}
6.3)测试主程序 main.c(共用)
c
#include <stdio.h>
#include "circular_queue.h" // 或 #include "link_queue.h"
// 注意:两者不能同时包含(类型名冲突),测试时选其一
int main() {
// 若测试循环队列:
CircularQueue q;
// 若测试链队列:
// LinkQueue q;
QueueInit(&q);
printf("=== 队列测试 ===\n");
printf("初始: 空=%s, 大小=%d\n",
QueueEmpty(&q) ? "是" : "否", QueueSize(&q));
// 入队
printf("\n--- 入队 ---\n");
for (int i = 1; i <= 5; i++) {
if (QueuePush(&q, i * 10)) {
printf("入队 %d, 队头=%d, 队尾=%d, 大小=%d\n",
i * 10, QueueFront(&q), QueueBack(&q), QueueSize(&q));
} else {
printf("队列已满,无法入队 %d\n", i * 10);
}
}
// 出队
printf("\n--- 出队 ---\n");
while (!QueueEmpty(&q)) {
printf("出队 %d, 剩余大小=%d\n", QueueFront(&q), QueueSize(&q));
QueuePop(&q);
}
QueueDestroy(&q);
return 0;
}
7)如何选择队列类别
| 场景 | 推荐实现 |
|---|---|
| 元素数量大致已知,追求速度 | 循环队列(无 malloc/free 开销) |
| 元素数量不确定,避免溢出 | 链队列(动态扩展) |
| 内存受限(如嵌入式) | 循环队列(固定内存) |
| 学习数据结构原理 | 两者都实现一遍! |
- 普通顺序队列:有缺陷,仅用于理解概念
- 循环队列 :顺序实现的正确姿势
- 链队列 :动态实现的简洁方案
实际开发中,链队列更常用(除非对性能极度敏感)