当前位置: 首页 > news >正文

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

目录

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

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

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. 初始化,空队列的头尾指针都为 NULLq->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 两个指针来满足性能要求,并细致地处理了入队和出队时的各种边界情况,最终构建了一个功能完整、健壮的链式队列。

http://www.dtcms.com/a/331129.html

相关文章:

  • python批量爬虫实战之windows主题爬取
  • 移位操作符技巧
  • 8. 函数简介
  • DeepSeek补全IBM MQ 9.4 REST API 执行命令的PPT
  • Mac chrome浏览器下载DevEco Studio 6.0.0 Beta2失败
  • 分布式锁—Redisson的公平锁
  • 线上故障定位:从报警到根因的实战指南
  • Eureka故障处理大汇总
  • 使用 Git Submodules 管理前后端分离项目
  • scikit-learn/sklearn学习|广义线性回归 Logistic regression的三种成本函数
  • Docker 核心技术:Namespace
  • Java毕业设计选题推荐 |基于SpringBoot的健身爱好线上互动与打卡社交平台系统 互动打卡小程序系统
  • 2019 GPT2原文 Language Models are Unsupervised Multitask Learners - Reading Notes
  • [激光原理与应用-274]:理论 - 波动光学 - 光是电磁波,无线电磁波可以通过天线接收和发送,为什么可见光不可以?
  • Visual Studio2019/2022离线安装完整教程(含闪退解决方法)
  • 无人机双目视觉设计要点概述!
  • SOD-YOLO:基于YOLO的无人机图像小目标检测增强方法
  • 值传递+move 优化数据传递
  • torchvision中数据集的使用与DataLoader 小土堆pytorch记录
  • Autoppt-AI驱动的演示文稿生成工具
  • 深入理解 RAG:检索增强生成技术详解
  • 通过机器学习框架实现Android手写识别输入功能
  • 【开源工具】基于硬件指纹的“一机一码”软件授权系统全实现(附完整源码)
  • MapReduce系统架构,颠覆了互联网分层架构的本质?
  • xiaozhi-esp32 仓库分析文档
  • 树莓派 4B 上部署 Minecraft PaperMC 1.20.x 的一键部署脚本
  • [论文阅读] 人工智能 + 软件工程 | 代码变更转自然语言生成中的幻觉问题研究解析
  • 智能家居主控板:智慧家庭的核心大脑
  • 华为实验 链路聚合
  • 实测对比:飞算JavaAI vs 人工编码,谁在效率与质量上更胜一筹?