数据结构队列

1. 队列的基本概念

1.1概念
  • 队列(queue)是限定在⼀端插⼊另⼀端删除的线性表。因此,对队列来说,插⼊那⼀端叫做队尾,插⼊叫⼊队,删除那⼀端叫队头,删除叫做出队。队列中的数据元素遵循先进先出(FIFO,First In First Out)的特性。
  • 队列的⼊队出队顺序就简单多了,⽆论如何操作,⼀种⼊队顺序只对应⼀种出队顺序。
  • 队列在计算机系统中应用非常广泛,例如操作系统中的任务调度 、打印任务排队 、消息队列 、广度优先搜索(BFS) 等场景,都离不开队列这种先进先出的数据结构。
1.2 队列的基本操作
c 复制代码
// 初始化队列
void QueueInit(LinkQueue* q); 
// 销毁队列
void QueueDestroy(LinkQueue* q); 
// 检测队列是否为空,空返回真,否则返回假
bool QueueEmpty(LinkQueue* q); 
// 获取队列中有效元素个数 
int QueueSize(LinkQueue* q);
// 获取队头元素
QDataType QueueFront(LinkQueue* q); 
// 将x⼊队列
void EnQueue(LinkQueue* q, QDataType x); 
// 出队并返回队头数据
QDataType DeQueue(LinkQueue* q); 

说明:QDataType 是队列中存储的数据类型,通常用 typedef 定义为 int 等具体类型;LinkQueue 是链式队列的结构体类型,下文会给出完整定义。

2. 队列的链式存储

2.1 链式队列的结构
  • 队列的链式存储,我们可以选⽤单链表结构,也可以选⽤双向链表结构。他们⼊队对应着在表尾插⼊,出队对应着在表头删除。当然我们完全没必要选择双向链表,因为单链表就可以⾼效实现,还省空间⼀些。双向链表没有优势,每个结点还要多存储⼀个前驱指针,使⽤它纯粹浪费了。

  • 其次这⾥我们选择单链表实现,⼊队就是尾插,出队就是头删。可以带头结点,也可以不带头结点,带头结点的结构略简单⼀点。

  • 链式队列的核心结构如下:用 head 指向队头结点,用 tail 指向队尾结点。入队时在 tail 之后插入新结点并更新 tail,出队时删除 head 指向的结点并更新 head 。带头结点时,head 指向哨兵位头结点,真正的队头元素是 head->next。

c 复制代码
// 链式队列的结点结构
typedef struct QNode {
    QDataType data;      // 数据域
    struct QNode* next;  // 指针域,指向下一个结点
} QNode;

// 链式队列的结构(带头结点)
typedef struct Queue {
    QNode* head;  // 指向哨兵位头结点
    QNode* tail;  // 指向队尾结点
} LinkQueue;

// 初始化队列:创建哨兵位头结点
void QueueInit(LinkQueue* q) {
    q->head = (QNode*)malloc(sizeof(QNode));
    if (q->head == NULL) {
        printf("内存申请失败\n");
        exit(-1);
    }
    q->head->next = NULL;
    q->tail = q->head;  // 空队列时,尾指针也指向哨兵位
}

// 销毁队列:释放所有结点
void QueueDestroy(LinkQueue* q) {
    QNode* cur = q->head;
    while (cur != NULL) {
        QNode* next = cur->next;
        free(cur);
        cur = next;
    }
    q->head = q->tail = NULL;
}

// 检测队列是否为空:空返回真,否则返回假
bool QueueEmpty(LinkQueue* q) {
    return q->head->next == NULL;
}

// 获取队列中有效元素个数
int QueueSize(LinkQueue* q) {
    int size = 0;
    QNode* cur = q->head->next;
    while (cur != NULL) {
        size++;
        cur = cur->next;
    }
    return size;
}

// 获取队头元素(不删除)
QDataType QueueFront(LinkQueue* q) {
    assert(q->head->next != NULL);  // 队列非空
    return q->head->next->data;
}
2.3 出队
c 复制代码
// 入队:在队尾插入新结点
void EnQueue(LinkQueue* q, QDataType x) {
    QNode* newnode = (QNode*)malloc(sizeof(QNode));
    if (newnode == NULL) {
        printf("内存申请失败\n");
        exit(-1);
    }
    newnode->data = x;
    newnode->next = NULL;

    q->tail->next = newnode;  // 新结点链接到队尾
    q->tail = newnode;        // 更新尾指针
}
``````c
// 出队:删除队头结点并返回其数据
QDataType DeQueue(LinkQueue* q) {
    assert(q != NULL);
    assert(q->head->next != NULL);  // 队列非空

    QNode* del = q->head->next;     // 待删除的队头结点
    QDataType ret = del->data;      // 保存数据

    q->head->next = del->next;      // 头结点指向下一个结点
    if (q->tail == del) {           // 删除的是最后一个结点
        q->tail = q->head;          // 尾指针回到哨兵位
    }
    free(del);                      // 释放结点
    return ret;
}

注意:当队列中只剩一个结点时,删除后 tail 必须重新指向哨兵位头结点,否则 tail 会变成野指针,后续入队操作将出错。

3. 队列的顺序存储

3.1 顺序队列的结构
  • 顺序存储结构实现队列,⼊队对应尾插,出队对应头删,⽽顺序存储结构直接头删的需要挪动数据时间复杂度为 O(n),如果实现队列的顺序存储结构效率太低,就⽆价值了。所以这⾥可以调整思路,出队头删时,不再挪动数据,⽽是移动队头的下标或指针。

  • 顺序队列的核心结构如下:用数组 a 存储数据,用 front 指向队头下标,用 rear 指向队尾下标。入队时在 rear 处写入数据并让 rear 后移,出队时让 front 后移即可,这样出队的时间复杂度降为 O(1)。

c 复制代码
// 顺序队列的结构
typedef struct SeqQueue {
    QDataType* a;   // 动态数组,存储数据
    int front;      // 队头下标
    int rear;       // 队尾下标
    int capacity;   // 数组容量
} SeqQueue;

// 初始化顺序队列
void SeqQueueInit(SeqQueue* q, int capacity) {
    q->a = (QDataType*)malloc(sizeof(QDataType) * capacity);
    if (q->a == NULL) {
        printf("内存申请失败\n");
        exit(-1);
    }
    q->front = 0;
    q->rear = 0;
    q->capacity = capacity;
}

// 入队:在队尾写入数据
void SeqQueuePush(SeqQueue* q, QDataType x) {
    assert(q->rear < q->capacity);  // 队列未满
    q->a[q->rear] = x;
    q->rear++;
}

// 出队:队头下标后移,不挪动数据
void SeqQueuePop(SeqQueue* q) {
    assert(q->front < q->rear);  // 队列非空
    q->front++;
}

// 获取队头元素
QDataType SeqQueueFront(SeqQueue* q) {
    assert(q->front < q->rear);  // 队列非空
    return q->a[q->front];
}

// 检测队列是否为空
bool SeqQueueEmpty(SeqQueue* q) {
    return q->front == q->rear;
}
3.2 顺序队列的问题
  • 顺序队列的⼀个经典问题就是假溢出问题,队列的存储空间(数组)中明明还有空闲位置,但由于队尾指针rear已经移动到了数组的末尾,导致⽆法再插⼊新元素,系统错误地判断为"队列已满"的现象,这种情况之所以被称为"假"溢出,是因为它并⾮真正的空间耗尽,⽽是⼀种由于数据结构设计缺陷导致的空间浪费。

  • 例如:数组容量为 5,连续入队 5 个元素后 rear 到达下标 5,此时再出队 3 个元素,front 变为 3,数组下标 0~2 已经空闲,但 rear 仍指向末尾,无法继续入队------这就是假溢出。

c 复制代码
// 演示假溢出问题
void DemoFakeOverflow() {
    SeqQueue q;
    SeqQueueInit(&q, 5);

    // 连续入队 5 个元素,rear 到达末尾
    for (int i = 1; i <= 5; i++) {
        SeqQueuePush(&q, i);
    }

    // 出队 3 个元素,front 变为 3,下标 0~2 空闲
    for (int i = 0; i < 3; i++) {
        SeqQueuePop(&q);
    }

    // 此时数组前 3 个位置空闲,但 rear 已到末尾,无法入队
    // SeqQueuePush(&q, 6);  // 触发 assert 失败,即"假溢出"
}

解决思路 :采用循环队列 ,让 rear 到达末尾后回绕到数组开头,从而复用空闲空间。循环队列中通常牺牲一个存储单元来区分队空与队满,即 (rear + 1) % capacity == front 表示队满,front == rear 表示队空。

相关推荐
老王爱玩车1 小时前
字符串和字符串函数
c语言·开发语言·数据结构·学习
扶风ff1 小时前
练题簿在线免费刷题:共享题库、学习任务、在线考试,岗位培训更方便
学习·小程序
JWASX1 小时前
Java 转 go 学习 - 并发编程(3)
学习·golang
传奇开心果编程1 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
H.莓飛1 小时前
【数据结构】二叉树
linux·数据结构·算法
欣欣之王来了1 小时前
5G网络基础:5G的核心特性,eMBB、uRLLC、mMTC的应用场景
网络·学习·计算机网络·安全·教程
Xudde.2 小时前
CVE-2026-24061漏洞复现
学习·安全
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
常州晟凯电子科技2 小时前
sigmaStar SSC8836Q/SSC8826Q开发笔记之环境搭建和SDK编译
笔记·嵌入式硬件·物联网·视频编解码