【数据结构初阶】队列的实现(链式队列 + 循环队列)--详解

一.队列的概念与结构

1.概念

概念:只允许在⼀端进⾏插⼊数据操作,在另⼀端进⾏删除数据操作的特殊线性表 ,队列具有先进先出FIFO(First In First Out)。

**⼊队列:**进⾏插⼊操作的⼀端称为队尾

**出队列:**进⾏删除操作的⼀端称为队头


2.结构

队列底层结构选型

队列也可以数组和链表的结构实现,使⽤链表的结构实现更优⼀些,因为如果使⽤数组的结构,出队列在数组头上出数据,效率会⽐较低。


a.队列的顺序存储结构

队列的顺序存储结构通常使用数组来实现,一般用队头指针(phead)和队尾指针(ptail)来标记队头和队尾的位置。

  • 入队:在队尾插入新元素,只需要修改队尾指针,不需要移动任何元素,时间复杂度为 O(1)。

  • 出队:删除队头元素后,需要将所有剩余元素整体往前移动一位,时间复杂度为 O(N)。

这种普通数组队列出队效率较低,因此实际中更常用循环队列来优化,让出队也达到 O(1)。


b.队列的链表存储结构

队列的链式存储结构使用链表来实现,定义两个指针:队头指针(phead)指向第一个节点,队尾指针(ptail)指向尾节点。

  • 空队列时,phead 和 ptail 都指向 NULL。

  • 入队(尾插):在链表尾部插入新节点,时间复杂度为 O(1)。

  • 出队(头删):删除链表头部节点,时间复杂度为 O(1)。

链式队列不需要像普通数组那样移动数据,入队和出队效率都较高,但每个节点需要额外的指针域,内存开销稍大。


二.队列的实现

用数组实现队列时,如果采用普通数组的方式,出队操作需要先删除队头数据,再将剩余所有元素整体往前移动一位,时间复杂度为 O(N)。当数据量较大时,这个开销是很明显的,效率太低,不适合实际使用。

相比之下,选择用单向链表来实现队列更合适。链式队列通过队头指针(phead)进行头删,通过队尾指针(ptail)进行尾插,入队和出队操作都只需要修改指针的指向,不需要移动任何数据,时间复杂度都是 O(1),效率很高。

至于双向链表,虽然也能实现队列,但队列只需要在一端插入(尾插)、另一端删除(头删),单向链表已经能够完美支持这两个操作。双向链表多维护了一个 prev 指针,在这个场景下发挥不出什么作用,反而增加了内存开销和代码复杂度,所以用单向链表就足够了。


1.创建文件

在实现队列之前,首先需要创建以下三个文件:

  • test.c ------ 主函数文件,用于测试队列的各个接口功能是否正常

  • Queue.c ------ 队列接口函数的具体实现文件,包含初始化、入队、出队、获取队头元素等操作

  • Queue.h ------ 队列的头文件,包含队列的类型定义、接口函数声明以及所需引用的头文件

采用这种模块化的文件组织方式,可以将队列的实现细节与测试代码分离,提高代码的可读性和可维护性,也便于后续复用和扩展。其中 Queue.h 负责声明,Queue.c 负责实现,test.c 负责验证。


2.Queue.h 头文件代码

cpp 复制代码
// Queue.h
// 链式结构:表示队列

#pragma once

#include <stdio.h>
#include <stdbool.h>   // bool 类型
#include <assert.h>    // assert 断言
#include <stdlib.h>    // malloc、free

typedef int QDataType; // 类型重命名,队列中元素类型先假设为 int

// 队列节点结构
typedef struct QueueNode
{
    struct QueueNode* next; // 指向下一个节点的指针
    QDataType data;         // 节点中存储的数据
} QueueNode;

// 队列的链式结构
typedef struct Queue
{
    QueueNode* head;        // 队头指针,指向第一个节点
    QueueNode* tail;        // 队尾指针,指向最后一个节点
    int size;               // 队列中有效元素的个数(可选,方便统计)
} Queue;

// 初始化队列
void QueueInit(Queue* pq);

// 销毁队列
void QueueDestroy(Queue* pq);

// 队尾入队列(尾插)
void QueuePush(Queue* pq, QDataType data);

// 队头出队列(头删)
void QueuePop(Queue* pq);

// 获取队列头部元素
QDataType QueueFront(Queue* pq);

// 获取队列尾部元素
QDataType QueueBack(Queue* pq);

// 获取队列中有效元素个数
int QueueSize(Queue* pq);

// 检测队列是否为空,如果为空返回 true,如果非空返回 false
bool QueueEmpty(Queue* pq);

注意这两点:

  • size 成员不是必须的,但加上后可以在 O(1) 时间内获取队列元素个数,否则需要遍历链表统计,时间复杂度为 O(N)

  • 队头指针 head 用于出队操作(头删),队尾指针 tail 用于入队操作(尾插),两者配合使用,入队和出队的时间复杂度都是 O(1)


三.Queue.c 中各个接口函数的实现

1.初始化队列

cpp 复制代码
// 初始化队列
void QueueInit(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针
    pq->head = pq->tail = NULL; // 头尾指针都置空
}

解析:

**初始化就是把队头和队尾指针都置成 NULL,表示这是一个空队列。**加上 assert 是为了保证传入的指针是有效的。


2.销毁队列

cpp 复制代码
// 销毁队列
void QueueDestroy(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针

    QueueNode* cur = pq->head; // 从队头开始遍历
    while (cur)
    {
        QueueNode* next = cur->next; // 保存下一个节点的地址
        free(cur);                  // 释放当前节点
        cur = next;                 // 移到下一个节点
    }

    pq->head = pq->tail = NULL; // 所有节点释放完后,头尾指针置空
}

解析:

**销毁队列需要把链表中所有节点都释放掉,防止内存泄漏。**从队头开始一个一个往后遍历,每次释放当前节点之前先保存下一个节点的地址,不然释放后就找不到下一个了。全部释放完之后,把 head 和 tail 置空。


3.队尾入队列(尾插)

cpp 复制代码
// 队尾入队列(尾插)
void QueuePush(Queue* pq, QDataType x)
{
    assert(pq);                // 断言,防止传入空指针

    // 动态申请一个新节点
    QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
    if (newnode == NULL)       // 检查内存申请是否成功
    {
        printf("malloc fail\n");
        exit(-1);
    }

    newnode->data = x;         // 存入数据
    newnode->next = NULL;      // 新节点的 next 置空(作为新的尾节点)

    // 如果队列为空,头尾指针都指向新节点
    if (pq->head == NULL)
    {
        pq->head = pq->tail = newnode;
    }
    else  // 队列不为空,链接到当前尾节点后面
    {
        pq->tail->next = newnode;
        pq->tail = newnode;    // 更新尾指针
    }
}

解析:

**入队就是在链表尾部插入一个新节点。**先 malloc 一个新节点,把数据存进去,next 置空。然后分两种情况:如果队列为空,头尾指针都指向这个新节点;如果队列不为空,就把当前尾节点的 next 指向新节点,然后更新尾指针。这样入队操作的时间复杂度是 O(1)。


4.队头出队列(头删)

cpp 复制代码
// 队头出队列(头删)
void QueuePop(Queue* pq)
{
    assert(pq);                     // 断言,防止传入空指针

    // 暴力处理方式(推荐):
    assert(!QueueEmpty(pq));        // 断言,确保队列不为空,空队列不能出队

    QueueNode* next = pq->head->next; // 记录第二个节点的地址
    free(pq->head);                  // 释放队头节点
    pq->head = next;                 // 更新队头指针

    // 如果队列中只有一个节点,删除后队列为空,尾指针也要置空
    if (pq->head == NULL)
    {
        pq->tail = NULL;
    }
}

cpp 复制代码
// 写法二(也是正确的)
void QueuePop(Queue* pq)
{
    assert(pq);                     // 断言,防止传入空指针
    assert(!QueueEmpty(pq));        // 断言,确保队列不为空

    if (pq->head->next == NULL)     // 队列中只有一个节点
    {
        free(pq->head);
        pq->head = pq->tail = NULL; // 头尾指针都置空
    }
    else                            // 队列中有多个节点
    {
        QueueNode* next = pq->head->next; // 记录第二个节点的地址
        free(pq->head);                  // 释放队头节点
        pq->head = next;                 // 更新队头指针
    }
}

解析:

出队就是删除链表的头节点

写法一先保存第二个节点的地址,然后释放头节点,再更新头指针。如果删完发现链表空了,就把尾指针也置空。

写法二则是分两种情况处理:如果只有一个节点,释放后头尾指针都置空;如果多个节点,就正常头删。

两种写法都行,写法一代码更简洁一些。关键点在于:链表只有一个节点时,要单独处理,否则 tail 会变成野指针。


5.获取队列头部元素

cpp 复制代码
// 获取队列头部元素
QDataType QueueFront(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针
    assert(!QueueEmpty(pq));   // 断言,确保队列不为空

    return pq->head->data;     // 返回队头节点的数据
}

解析:

这个函数只获取队头元素的值,不删除它。直接返回 head->data 就行了。用 assert 保证队列不为空,否则访问空指针会出问题。


6.获取队列尾部元素

cpp 复制代码
// 获取队列尾部元素
QDataType QueueBack(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针
    assert(!QueueEmpty(pq));   // 断言,确保队列不为空

    return pq->tail->data;     // 返回队尾节点的数据
}

解析:

和上面类似,只是换成了返回队尾元素。因为有 tail 指针,直接 tail->data 就拿到了,时间复杂度 O(1)。如果没有 tail 指针,就得遍历到链表尾部,那就变成 O(N) 了。这也是为什么我们要在队列结构体里同时维护 head 和 tail 两个指针。


7.获取队列中有效元素个数

cpp 复制代码
// 获取队列中有效元素个数
int QueueSize(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针

    int n = 0;
    QueueNode* cur = pq->head; // 从队头开始遍历
    while (cur)
    {
        n++;
        cur = cur->next;
    }
    return n;
}

解析:

这个函数返回队列中有效元素的个数。从队头开始遍历整个链表,每经过一个节点计数加一,直到遍历完所有节点。时间复杂度是 O(N)。

如果频繁调用这个接口获取元素个数,每次都要遍历整个链表效率就太低了,最直观的改进办法就是在队列结构体里加一个 size 成员变量,入队时 size++,出队时 size--,这样获取元素个数直接返回 size 就行,时间复杂度 O(1)。

8.检测队列是否为空

cpp 复制代码
// 检测队列是否为空,如果为空返回 true,如果不为空返回 false
bool QueueEmpty(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针

    return pq->head == NULL;   // 头指针为空表示队列为空
}

解析:

判断队列是否为空,只需要看队头指针是不是 NULL。如果 head 为 NULL,说明队列里没有节点,返回 true;否则返回 false。

这里只判断 head 就够了,因为队列为空时 head 一定为 NULL,tail 也为 NULL,但反过来如果 head 不为空,队列肯定不为空。

四.整合代码

Queue.h 头文件

cpp 复制代码
// Queue.h
// 链式结构:表示队列

#pragma once

#include <stdio.h>
#include <stdbool.h>   // bool 类型
#include <assert.h>    // assert 断言
#include <stdlib.h>    // malloc、free

typedef int QDataType; // 类型重命名,队列中元素类型先假设为 int

// 队列节点结构
typedef struct QueueNode
{
    struct QueueNode* next; // 指向下一个节点的指针
    QDataType data;         // 节点中存储的数据
} QueueNode;

// 队列的链式结构
typedef struct Queue
{
    QueueNode* head;        // 队头指针,指向第一个节点
    QueueNode* tail;        // 队尾指针,指向最后一个节点
    int size;               // 队列中有效元素的个数(方便统计)
} Queue;

// 初始化队列
void QueueInit(Queue* pq);

// 销毁队列
void QueueDestroy(Queue* pq);

// 队尾入队列(尾插)
void QueuePush(Queue* pq, QDataType data);

// 队头出队列(头删)
void QueuePop(Queue* pq);

// 获取队列头部元素
QDataType QueueFront(Queue* pq);

// 获取队列尾部元素
QDataType QueueBack(Queue* pq);

// 获取队列中有效元素个数
int QueueSize(Queue* pq);

// 检测队列是否为空,如果为空返回 true,如果非空返回 false
bool QueueEmpty(Queue* pq);

Queue.c 源文件

cpp 复制代码
// Queue.c
// 队列接口函数的实现

#include "Queue.h"

// 1、初始化队列
void QueueInit(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针
    pq->head = pq->tail = NULL; // 头尾指针都置空
    pq->size = 0;              // 元素个数归零
}

// 2、销毁队列
void QueueDestroy(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针

    QueueNode* cur = pq->head; // 从队头开始遍历
    while (cur)
    {
        QueueNode* next = cur->next; // 保存下一个节点的地址
        free(cur);                  // 释放当前节点
        cur = next;                 // 移到下一个节点
    }

    pq->head = pq->tail = NULL; // 所有节点释放完后,头尾指针置空
    pq->size = 0;              // 元素个数归零
}

// 3、队尾入队列(尾插)
void QueuePush(Queue* pq, QDataType x)
{
    assert(pq);                // 断言,防止传入空指针

    // 动态申请一个新节点
    QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
    if (newnode == NULL)       // 检查内存申请是否成功
    {
        printf("malloc fail\n");
        exit(-1);
    }

    newnode->data = x;         // 存入数据
    newnode->next = NULL;      // 新节点的 next 置空(作为新的尾节点)

    // 如果队列为空,头尾指针都指向新节点
    if (pq->head == NULL)
    {
        pq->head = pq->tail = newnode;
    }
    else  // 队列不为空,链接到当前尾节点后面
    {
        pq->tail->next = newnode;
        pq->tail = newnode;    // 更新尾指针
    }
    pq->size++;                // 元素个数加一
}

// 4、队头出队列(头删)
void QueuePop(Queue* pq)
{
    assert(pq);                     // 断言,防止传入空指针
    assert(!QueueEmpty(pq));        // 断言,确保队列不为空

    QueueNode* next = pq->head->next; // 保存第二个节点的地址
    free(pq->head);                  // 释放队头节点
    pq->head = next;                 // 更新队头指针

    // 如果队列中只剩一个节点,删除后队列为空,尾指针也要置空
    if (pq->head == NULL)
    {
        pq->tail = NULL;
    }
    pq->size--;                // 元素个数减一
}

// 5、获取队列头部元素
QDataType QueueFront(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针
    assert(!QueueEmpty(pq));   // 断言,确保队列不为空

    return pq->head->data;     // 返回队头节点的数据
}

// 6、获取队列尾部元素
QDataType QueueBack(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针
    assert(!QueueEmpty(pq));   // 断言,确保队列不为空

    return pq->tail->data;     // 返回队尾节点的数据
}

// 7、获取队列中有效元素个数
int QueueSize(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针

    return pq->size;           // 直接返回 size,时间复杂度 O(1)
}

// 8、检测队列是否为空
bool QueueEmpty(Queue* pq)
{
    assert(pq);                // 断言,防止传入空指针

    return pq->head == NULL;   // 头指针为空表示队列为空
}

test.c 测试文件

cpp 复制代码
// test.c
// 主函数及功能测试 ------ 覆盖所有接口函数

#include "Queue.h"

// 测试1:覆盖接口 ------ QueueInit、QueuePush、QueuePop、QueueFront、QueueSize、QueueEmpty、QueueDestroy
void TestQueue1()
{

    Queue q;
    QueueInit(&q);              //  测试初始化

    printf("入队:1 2 3 4 5\n");
    QueuePush(&q, 1);           //  测试入队(尾插)
    QueuePush(&q, 2);
    QueuePush(&q, 3);
    QueuePush(&q, 4);
    QueuePush(&q, 5);

    printf("队列中元素个数:%d\n", QueueSize(&q));   // 测试获取元素个数

    printf("出队顺序:");
    while (!QueueEmpty(&q))      //  测试判空
    {
        printf("%d ", QueueFront(&q));  //  测试获取队头元素
        QueuePop(&q);            //  测试出队(头删)
    }
    printf("\n");

    printf("出队后元素个数:%d\n", QueueSize(&q));   //  测试获取元素个数
    printf("\n");

    QueueDestroy(&q);            //  测试销毁队列
}

// 测试2:覆盖接口 ------ QueueInit、QueuePush、QueueFront、QueueBack、QueueDestroy
void TestQueue2()
{

    Queue q;
    QueueInit(&q);              //  测试初始化

    QueuePush(&q, 10);          //  测试入队
    QueuePush(&q, 20);
    QueuePush(&q, 30);

    printf("队头元素:%d\n", QueueFront(&q));   //  测试获取队头
    printf("队尾元素:%d\n", QueueBack(&q));    //  测试获取队尾
    printf("\n");

    QueueDestroy(&q);            //  测试销毁
}

// 测试3:覆盖接口 ------ QueueInit、QueuePush、QueuePop、QueueFront、QueueSize、QueueEmpty、QueueDestroy
//       额外验证多个队列同时存在时互不干扰
void TestQueue3()
{

    Queue q1, q2;
    QueueInit(&q1);             //  测试初始化
    QueueInit(&q2);             //  测试初始化

    QueuePush(&q1, 1);          //  测试入队
    QueuePush(&q1, 2);
    QueuePush(&q1, 3);

    QueuePush(&q2, 10);         // 测试入队
    QueuePush(&q2, 20);
    QueuePush(&q2, 30);
    QueuePush(&q2, 40);

    printf("q1 元素个数:%d\n", QueueSize(&q1));   //  测试获取元素个数
    printf("q2 元素个数:%d\n", QueueSize(&q2));

    printf("q1 出队:");
    while (!QueueEmpty(&q1))     // 测试判空
    {
        printf("%d ", QueueFront(&q1));   //  测试获取队头
        QueuePop(&q1);           //  测试出队
    }
    printf("\n");

    printf("q2 出队:");
    while (!QueueEmpty(&q2))
    {
        printf("%d ", QueueFront(&q2));
        QueuePop(&q2);
    }
    printf("\n");
    printf("\n");

    QueueDestroy(&q1);           //  测试销毁
    QueueDestroy(&q2);
}

int main()
{
    TestQueue1();   // 初始话、入队、出队、获取队头、获取元素个数、判空、销毁
    TestQueue2();   // 初始化、入队、获取队头、获取队尾、销毁
    TestQueue3();   // 初始化、入队、出队、获取队头、获取元素个数、判空、销毁,并验证多队列独立性

    return 0;
}

五.测试功能

1.测试1:基本入队出队功能


2.测试2:获取队头和队尾元素


3.测试3:多队列独立运行


六.拓展学习

循环队列的概念与结构

1.概念

实际中还有⼀种特殊的队列叫循环队列,环形队列⾸尾相连成环,环形队列可以使⽤数组实现,也可以使⽤循环链表实现。

**循环队列的好处是:**出队的时候不用挪动数据,只需移动队头指针就行了,入队和出队的时间复杂度都是 O(1)。解决了普通数组队列出队要挪动数据、效率低的问题。

2.结构

3.空循环队列

队列为空时,队头指针(front)和队尾指针(rear)指向同一个位置,即:

cpp 复制代码
front == rear

初始状态就是空的,这时候 front 和 rear 都指向数组的第一个位置。


4.满的循环队列

两种判断方式:

方式一:牺牲一个存储单元

(rear + 1) % capacity == front 时,认为队列已满。这种方式相当于把最后一个位置空出来不用,专门用来区分队空和队满。

判断队空:front == rear

判断队满:(rear + 1) % capacity == front

这里用取模运算就是为了让 rear 加 1 之后能回到数组开头,形成循环的效果。

方式二:增加 size 变量

在结构体里加一个 size 变量,入队时 size++,出队时 size--。判断队满直接看 size == capacity 就行了。好处是不浪费空间,但要多维护一个变量。


思考:为什么方式一中 rear 位置不存储数据?

如果所有位置都存了数据,那么 front == rear 的时候,你分不清这是队空还是队满。因为队列为空时 front == rear,队列满了之后,如果继续插入,rear 绕了一圈又回到 front 的位置,还是 front == rear,这就矛盾了。

所以为了能区分这两种情况,我们常用的做法是故意浪费一个位置,不存数据。当 rear 的下一个位置是 front 时,就说明队列满了,不能再插了。

小小总结方便大家理解:

循环队列说白了就是数组围成一个圈,通过取模运算让指针转起来。核心就是要搞清楚怎么判断空和满。一般用方式一比较多,虽然浪费一个空间,但逻辑清晰,写起来也方便。方式二更节约空间,但要多维护一个 size 变量,看个人习惯选择。

用表格让两种方式对比:

方式 队空条件 队满条件 是否浪费空间 实现难度
牺牲一个位置 front == rear (rear+1) % k == front 浪费一个 简单
增加 size 变量 size == 0 size == capacity 不浪费 稍微复杂一点

5.循环队列的小实验

cpp 复制代码
#include <stdio.h>
#include <stdbool.h>

#define CAPACITY 5

typedef struct {
    int a[CAPACITY];
    int front;
    int rear;
} MyCircularQueue;

void init(MyCircularQueue* q)
{
    q->front = 0;
    q->rear = 0;
}

bool empty(MyCircularQueue* q)
{
    return q->front == q->rear;
}

bool full(MyCircularQueue* q)
{
    return (q->rear + 1) % CAPACITY == q->front;
}

void push(MyCircularQueue* q, int val)
{
    if (full(q)) {
        printf("队列满了,%d 入队失败\n", val);
        return;
    }
    q->a[q->rear] = val;
    q->rear = (q->rear + 1) % CAPACITY;
    printf("%d 入队成功,rear = %d\n", val, q->rear);
}

void pop(MyCircularQueue* q)
{
    if (empty(q)) {
        printf("队列空了,出队失败\n");
        return;
    }
    printf("%d 出队成功,front = %d\n", q->a[q->front], q->front);
    q->front = (q->front + 1) % CAPACITY;
}

void print(MyCircularQueue* q)
{
    printf("front = %d, rear = %d:", q->front, q->rear);
    int i = q->front;
    while (i != q->rear) {
        printf("%d ", q->a[i]);
        i = (i + 1) % CAPACITY;
    }
    printf("\n");
}

int main()
{
    MyCircularQueue q;
    init(&q);

    printf("循环队列小实验\n");
    printf("容量 %d,实际存 %d 个\n\n", CAPACITY, CAPACITY - 1);

    push(&q, 1);
    push(&q, 2);
    push(&q, 3);
    push(&q, 4);
    print(&q);

    push(&q, 5);
    print(&q);

    pop(&q);
    pop(&q);
    print(&q);

    push(&q, 5);
    push(&q, 6);
    print(&q);

    while (!empty(&q)) {
        pop(&q);
    }
    printf("全部出队后:");
    print(&q);

    return 0;
}

运行结果如下:

代码解析

cpp 复制代码
#include <stdio.h>
#include <stdbool.h>

#define CAPACITY 5    // 数组容量

// 队列结构体
typedef struct {
    int a[CAPACITY];  // 数组存放数据
    int front;        // 队头下标
    int rear;         // 队尾下标
} MyCircularQueue;

这里定义了一个循环队列,front 指向队头,rear 指向队尾的下一个位置。


cpp 复制代码
void init(MyCircularQueue* q)
{
    q->front = 0;
    q->rear = 0;
}

初始状态,front 和 rear 都指向 0,队列为空。


cpp 复制代码
bool empty(MyCircularQueue* q)
{
    return q->front == q->rear;
}

front 和 rear 相遇,说明队列里没有元素。


cpp 复制代码
bool full(MyCircularQueue* q)
{
    return (q->rear + 1) % CAPACITY == q->front;
}

rear 的下一个位置是 front,说明队列满了,所以浪费一个位置不用。


cpp 复制代码
void push(MyCircularQueue* q, int val)
{
    if (full(q)) {
        printf("队列满了,%d 入队失败\n", val);
        return;
    }
    q->a[q->rear] = val;                  // 在 rear 位置放数据
    q->rear = (q->rear + 1) % CAPACITY;   // rear 往后移,取模实现循环
    printf("%d 入队成功,rear = %d\n", val, q->rear);
}

入队就是在 rear 位置放数据,然后 rear 往后走一步。取模让 rear 能从末尾回到开头。


cpp 复制代码
void pop(MyCircularQueue* q)
{
    if (empty(q)) {
        printf("队列空了,出队失败\n");
        return;
    }
    printf("%d 出队成功,front = %d\n", q->a[q->front], q->front);
    q->front = (q->front + 1) % CAPACITY;  // front 往后移,取模实现循环
}

出队就是 front 往后走一步,逻辑上把队头删掉了,数据不用真的清除。


实验说明

这个实验演示了什么?

用 5 个空间的数组实现队列,实际只能存 4 个数据,通过 front 和 rear 两个指针来标记队头和队尾。

关键现象:

入队 1 2 3 4 后,rear = 4,队列满了,5 入队失败。然后出队 1 和 2,front 变成 2,这时候数组前面下标 0 和 1 的位置就空出来了。接着入队 5,rear 从 4 变成 0,入队 6,rear 从 0 变成 1。说明 rear 通过取模运算回到了数组开头,把前面空出来的位置重新利用上了。

得出结论:

循环队列解决了普通数组队列"出队后前面位置空着却用不上"的问题,空间可以反复使用,而且出队不用挪数据,效率高。核心就是靠 % CAPACITY 取模运算让指针在数组里转圈。

相关推荐
草莓熊Lotso40 分钟前
【Linux网络】深入理解Linux IO多路复用:select服务器完善、内核原理与poll实战
linux·运维·服务器·c语言·网络·c++
king_linlin1 小时前
算法基础——算法复杂度
c语言·开发语言·数据结构·算法
二十雨辰1 小时前
[爬虫]-解析
开发语言·python
zzq77971 小时前
Android 16 API 36 升级后 APP 加固兼容性问题解析
android·开发语言·安全·kotlin·安卓·安全架构
keyipatience1 小时前
日志和线程池
java·开发语言
xxwxx__1 小时前
51单片机电机控制全攻略:直流电机、舵机与步进电机
c语言·单片机·嵌入式硬件·51单片机
码云数智-园园1 小时前
建站平台有哪些?建站工具怎么选
开发语言
nVisual1 小时前
01-环境监控集成方案
运维·服务器·开发语言·网络·数据库·数据中心布线·综合布线管理软件
此生决int1 小时前
深入理解C++系列(04)——类和对象(下)
开发语言·c++