数据结构:用链表实现队列(Implementing Queue Using List)

发布于:2025-08-15 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

第一性原理:为什么需要链表?

设计链表的节点和队列结构

1. 积木:节点 (Node)

2. 蓝图:队列的结构 (Queue)

第2步:队列的诞生与状态检查

创建一个空队列

 判断队列是否为空

第3步:核心操作 (入队与出队)

入队 (Enqueue)

出队 (Dequeue)

第4步:善后工作 (销毁队列)


我们再次从第一性原理出发,承接上文,这次我们来探讨如何用链表来实现一个队列。我们同样会一步步分析,发现问题,并逐步完善代码。

数据结构:队列(Queue)与循环队列(Circular Queue)-CSDN博客

第一性原理:为什么需要链表?

我们已经知道,基于数组的循环队列有一个天生的“缺陷”:容量固定

在创建队列时,我们必须预先指定其最大容量。如果业务需求变化,元素数量可能远超预期,队列就会“溢出”;如果预估过大,则会浪费内存。

如何解决这个问题?我们需要一个“用多少,拿多少”的动态结构。这正是链表的拿手好戏。

我们需要一个数据结构,能够高效地(时间复杂度 O(1))在头部移除元素,并在尾部添加元素。


设计链表的节点和队列结构

链表是由一个个“节点”串联起来的。所以,我们首先需要设计这个最基本的“积木”。

1. 积木:节点 (Node)

一个节点需要包含什么信息?

  1. 数据本身 (int data)

  2. 指向下一个节点的指针 (struct Node* next),这是把它“链接”起来的关键。

【代码实现 1:节点结构体定义】

#include <stdio.h>
#include <stdlib.h>

// 链表节点,是构成我们队列的基本单元
typedef struct Node {
    int data;
    struct Node* next;
} Node;

2. 蓝图:队列的结构 (Queue)

如何用链表来组织,才能满足 O(1) 的头删和尾增?

如果我们有一个指针指向链表的头节点,我们称之为 front

那么删除头节点的操作就是把 front 指向 front->next,这本身是 O(1) 的。

  • 从头部删除 (Dequeue):

  • 在尾部添加 (Enqueue):

如果我们只有一个 front 指针,为了找到尾部,我们必须从头遍历整个链表,时间复杂度是 O(N),这太慢了,不符合我们的要求。

为了实现 O(1) 的尾部添加,我们必须始终保留一个直接指向尾节点的指针。我们称之为 rear

因此,我们的队列结构需要两个指针:一个指向头,一个指向尾。

【代码实现 2:链式队列结构体定义】

// 链式队列的蓝图
typedef struct {
    Node* front; // 指向队头节点
    Node* rear;  // 指向队尾节点
} LinkedQueue;

这个结构比数组实现更简洁,因为它不需要关心容量和下标。


第2步:队列的诞生与状态检查

创建一个空队列

一个空的链式队列是什么样的?它没有任何节点。因此,它的 frontrear 指针都应该指向 NULL

【代码实现 3:创建队列函数】

// 功能:创建一个空的链式队列
LinkedQueue* createLinkedQueue() {
    // 1. 为队列结构体本身分配内存
    LinkedQueue* q = (LinkedQueue*)malloc(sizeof(LinkedQueue));
    if (!q) {
        perror("Failed to allocate memory for LinkedQueue");
        return NULL;
    }

    // 2. 初始化,空队列的头尾指针都为 NULL
    q->front = NULL;
    q->rear = NULL;

    return q;
}

 判断队列是否为空

这比数组实现简单得多。只要 front 指针是 NULL,队列就是空的。

【代码实现 4:判空函数】

// 功能:判断队列是否为空
int isLinkedQueueEmpty(LinkedQueue* q) {
    // 如果 front 为 NULL,队列必为空
    return q->front == NULL;
}

注意:链式队列理论上没有“满”的状态(除非整个计算机的内存耗尽),所以我们不需要 isFull 函数。


第3步:核心操作 (入队与出队)

这是链式队列最关键的部分,包含了一些必须小心处理的“边界情况”。

入队 (Enqueue)

向队尾添加一个元素 value

  1. 创建新节点:无论如何,我们都需要先根据 value 创建一个新的节点。

  2. 处理边界情况:如果当前队列是空的 (frontNULL),那么这个新节点既是队头也是队尾。所以 frontrear 都应该指向这个新节点。

  3. 处理通用情况:如果队列不为空,我们只需做两件事:

a. 让当前的队尾节点(q->rear)的 next 指针指向新节点。

b. 更新队尾指针 q->rear,让它指向这个新节点。

【代码实现 5:入队函数】

// 功能:将元素 value 加入队尾
void enqueueLinked(LinkedQueue* q, int value) {
    // 1. 无论如何,都先创建新节点
    Node* newNode = (Node*)malloc(sizeof(Node));
    if (!newNode) {
        perror("Failed to allocate memory for new node");
        return;
    }
    newNode->data = value;
    newNode->next = NULL; // 新节点总是被加在最后,所以它的 next 永远是 NULL

    // 2. 处理队列为空的特殊情况
    if (isLinkedQueueEmpty(q)) {
        q->front = newNode;
        q->rear = newNode;
    } else {
        // 3. 通用情况:将新节点链接到队尾,并更新队尾指针
        q->rear->next = newNode;
        q->rear = newNode;
    }
}

出队 (Dequeue)

从队头取出一个元素。

  1. 检查:队列是否为空?空则无法出队。

  2. 获取数据:要返回的数据在 front 指针指向的节点里。

  3. 移动指针:将 front 指针移动到下一个节点 (q->front = q->front->next)。

  4. 释放内存:原来的头节点已经“游离”出队列了,必须用 free 释放它的内存。

  5. 处理最关键的边界情况

如果出队的是队列中最后一个元素,那么在移动 front 指针后,front 会变为 NULL

此时队列变空,但 rear 指针还指向刚刚被释放的那个无效内存地址!

这非常危险。我们必须在这种情况下,同时将 rear 指针也设置为 NULL

【代码实现 6:出队函数】

// 功能:从队头取出一个元素,并通过指针 pValue 返回
int dequeueLinked(LinkedQueue* q, int* pValue) {
    // 1. 检查是否为空
    if (isLinkedQueueEmpty(q)) {
        printf("出队失败:队列为空。\n");
        return 0; // 失败
    }

    // 为了稍后释放,临时保存旧的头节点
    Node* temp = q->front;

    // 2. 获取数据
    *pValue = temp->data;

    // 3. 移动 front 指针到下一个节点
    q->front = q->front->next;

    // 4. 处理最关键的边界:如果出队后队列变空了
    if (q->front == NULL) {
        // 必须同时更新 rear 指针,否则 rear 会成为一个危险的“野指针”
        q->rear = NULL;
    }

    // 5. 释放旧的头节点内存
    free(temp);

    return 1; // 成功
}

第4步:善后工作 (销毁队列)

销毁整个队列意味着要释放所有节点的内存,以及队列结构体本身的内存。

最简单的方法是利用我们已经写好的 dequeueLinked 函数。

【代码实现 7:销毁函数】

// 功能:销毁整个队列,释放所有内存
void destroyLinkedQueue(LinkedQueue* q) {
    int dummyValue; // 用于接收出队元素,我们不关心它的值
    // 不断地从队列中取出元素,直到队列为空
    // dequeueLinked 函数会自动释放每个节点的内存
    while (!isLinkedQueueEmpty(q)) {
        dequeueLinked(q, &dummyValue);
    }
    // 最后,释放队列结构体本身的内存
    free(q);
}

通过以上步骤,我们从链表的基本特性出发,推导出了需要 frontrear 两个指针来满足性能要求,并细致地处理了入队和出队时的各种边界情况,最终构建了一个功能完整、健壮的链式队列。