C语言循环队列的实现与分析

引言

队列(Queue)和栈类似,相同点是都属于线性结构,不同点是栈遵循“后进先出”原则,而队列遵循“先进先出”的原则,也被成为“FIFO”结构,就是“First Input First Output”

image-20250506182818420

数据结构中的队列的两端都允许操作,只不过要求数据只能从队列的一端插入,从队列的另一端删除,可以把队列理解为一根水管,水管有进水口和出水口。一般把允许数据插入的一端称为队尾(Tail或者Rear),一般把允许删除数据的一端称为队头队首(Head或者Front)。

image-20250506182851744

  1. 把允许数据插入的一端称为队尾(允许数据插入到队列的操作称为入队,英文enqueue)

image-20250506182914472

  1. 把允许删除数据的一端称为队头(允许数据从队列删除的操作称为出队,英文dequeue)

image-20250506182926486

队列也属于线性结构,所以根据数据元素之间的物理关系来划分的话同样可以以数组或者链表为基础来实现队列的操作。

以数组为基础实现循环队列
如果以数组为基础,一般会把队列设置为循环队列,循环队列也被称为“环形缓冲区”,因为如果队列中的元素出队,则需要把该元素的后继元素整体向前移动,这是时间复杂度为O(n)的操作。

image-20250506182945308

如果数据出队之后不去移动后继元素又会导致数组的空间被浪费,为了解决该问题,可以把队列设置为循环队列,在移动数据的时候只需要移一次即可,所以时间复杂度就是O(1)。

image-20250506182957314

1. 定义数据类型与队列结构

1.1 数据类型定义

循环队列的元素类型可以根据需要进行更改。在此示例中,我们将队列的数据类型定义为 int,并且可以根据需求将其更改为其他类型,如 floatchar

// 循环队列的数据类型
typedef int DataType_t;

1.2 队列结构体定义

我们需要定义一个结构体来管理循环队列的相关信息,包括队列的数据存储、队列的容量、队头和队尾指针。结构体 CirQueue_t 将包含以下成员:

  • addr:指向存储数据的数组。
  • size:队列的最大容量。
  • rear:队尾指针,指向下一个入队位置。
  • front:队首指针,指向当前出队的位置。
// 构造循环队列的各项参数(首地址,容量大小,队尾下标,队首下标)
typedef struct  CircularQueue
{
    DataType_t   *addr; // 记录循环队列首元素地址
    unsigned int size;  // 记录循环队列容量大小
    int          rear;  // 记录循环队列队尾元素下标
    int          front; // 记录循环队列队首元素下标
} CirQueue_t;

2. 创建循环队列

函数 CirQueue_create 用于分配内存并初始化循环队列。它会返回一个指向 CirQueue_t 结构体的指针,表示创建的队列。

/**
 * @name      CirQueue_create
 * @brief     创建循环队列并对循环队列进行初始化
 * @param     size 循环队列的大小
 * @return
 *      @retval    manager 循环队列的管理结构体
 * @date      2025/04/26
 * @version   1.0
 * @note      
 */
CirQueue_t *CirQueue_create(unsigned size)
{
    // 1.利用calloc给管理结构体manager申请一块堆内存
    CirQueue_t *manager = (CirQueue_t *)calloc(1, sizeof(CirQueue_t));
    // 错误处理
    if (manager == NULL) {
        perror("calloc memory for manager is failed!");
        exit(-1); // 程序异常终止
    }
    
    // 2.利用calloc给循环队列元素申请一块堆内存
    manager->addr = (DataType_t*)calloc(size, sizeof(DataType_t));
    // 错误处理
    if (manager->addr == NULL) {
        perror("calloc memory for addr is failed!");
        free(manager);
        exit(-1);
    }

    // 3.对循环队列容量大小进行初始化
    manager->size = size;
    // 4.循环队列队尾下标进行初始化
    manager->rear = 0;
    // 5.循环队列队首下标进行初始化
    manager->front = 0;
    
    return manager;
}
  • calloc:使用 calloc 分配内存并初始化为零,防止出现未初始化的内存。
  • manager->size = size;:初始化队列的容量为指定的 size
  • manager->rear = 0;manager->front = 0;:初始化队尾和队首指针为 0,表示队列为空。

3. 判断队列是否已满

队列满的条件是队尾指针加 1 后与队首指针重合。通过该函数来判断队列是否已满。

/**
 * @name      CirQueue_IsFull
 * @brief     判断循环队列是否已满
 * @param     manager 循环队列的管理结构体
 * @return
 *      @retval true 循环队列已满   
 *      @retval false 循环队列未满   
 * @date      2025/04/26
 * @version   1.0
 * @note      
 */
bool CirQueue_IsFull(CirQueue_t *manager)
{
    return ((manager->rear + 1) % manager->size == manager->front) ? true : false;
}
  • ((manager->rear + 1) % manager->size == manager->front):判断队尾指针加 1 后是否与队首指针重合,这是队列满的标准。

4. 入队操作

CirQueue_Enqueue 函数将新元素加入队列,并更新队尾指针。如果队列已满,函数会返回 false

/**
 * @name      CirQueue_Enqueue
 * @brief     往循环队列入队
 * @param     data 入队的元素
 * @return
 *      @retval true 入队成功   
 *      @retval false 入队失败   
 * @date      2025/04/26
 * @version   1.0
 * @note      
 */
bool CirQueue_Enqueue(CirQueue_t *manager, DataType_t data)
{
    // 判断循环队列是否已满
    if (CirQueue_IsFull(manager)) {
        printf("CircularQueue is full!\n");
        return false;
    }
    
    // 如果没满,则往尾部添加元素
    manager->addr[manager->rear] = data;
    
    // 防止队尾下标越界
    manager->rear = (manager->rear + 1) % manager->size;
    
    return true;
}
  • CirQueue_IsFull(manager):判断队列是否已满,如果已满则返回 false
  • manager->addr[manager->rear] = data;:将新数据插入到队尾。
  • manager->rear = (manager->rear + 1) % manager->size;:更新队尾指针,并使用模运算避免越界。

5. 判断队列是否为空

队列为空的条件是队尾指针等于队首指针。此函数用于判断队列是否为空。

/**
 * @name      CirQueue_IsEmpty
 * @brief     判断循环队列是否为空
 * @param     manager 循环队列的管理结构体
 * @return
 *      @retval true 循环队列空   
 *      @retval false 循环队列不为空   
 * @date      2025/04/26
 * @version   1.0
 * @note      
 */
bool CirQueue_IsEmpty(CirQueue_t *manager)
{
    return (manager->rear == manager->front) ? true : false;
}

6. 出队操作

CirQueue_Dequeue 函数从队首取出元素,并更新队首指针。如果队列为空,函数会返回 false

/**
 * @name      CirQueue_Dequeue
 * @brief     循环队列出队
 * @param     data 出队的元素
 * @return
 *      @retval true 出队成功   
 *      @retval false 出队失败    
 * @date      2025/04/26
 * @version   1.0
 * @note      
 */
bool CirQueue_Dequeue(CirQueue_t *manager, DataType_t *data)
{
    // 1.判断循环队列是否为空
    if (CirQueue_IsEmpty(manager)) {
        printf("CircularQueue is empty!\n");
        return false;
    }

    // 获取队首元素
    *data = manager->addr[manager->front];
    
    // 更新队首指针,防止下标越界
    manager->front = (manager->front + 1) % manager->size;

    return true;
}

7. 测试代码

在主函数中,我们创建了一个大小为 5 的队列,并执行了入队和出队操作。

int main() {
    CirQueue_t *queue = CirQueue_create(5); // 创建一个大小为5的队列

    CirQueue_Enqueue(queue, 10);
    CirQueue_Enqueue(queue, 20);
    CirQueue_Enqueue(queue, 30);

    DataType_t data;
    if (CirQueue_Dequeue(queue, &data)) {
        printf("Dequeued: %d\n", data); // 应该输出 10
    }

    if (CirQueue_Dequeue(queue, &data)) {
        printf("Dequeued: %d\n", data); // 应该输出 20
    }

    CirQueue_Enqueue(queue, 40);
    CirQueue_Enqueue(queue, 50);
    CirQueue_Enqueue(queue, 60); // 应该入队成功

    if (CirQueue_Dequeue(queue, &data)) {
        printf("Dequeued: %d\n", data); // 应该输出 30
    }

    return 0;
}

运行结果

如下:

Dequeued: 10
Dequeued: 20
Dequeued: 30

总结

本文实现了一个简单的循环队列,包括队列的创建、入队、出队、判断队列是否为空或已满等基本操作。通过使用模运算,我们确保了队列的循环特性,从而能够高效地利用内存空间。

posted @ 2025-05-06 19:12  九思0404  阅读(122)  评论(0)    收藏  举报