图文并茂——队列的实现
每博一文案
说好的来日方长,却输给了世事无常,本以为相扶到老,没想到随着
岁月的流逝,竟变得隔山隔海般冷漠。
人生就像坐公交车,能陪你挤占的人不会少数,但却少有人,能陪
你从起点坐到终点。这世间分分合合,本就是人生常态,强求只是空增烦恼。
这辈子别人是我们路上的过客,而我们也是别人窗边的风景,人生海海,
人世间很多的相遇。它后来都成了擦肩而过。
我们决定不了一个人的出现,也挽留不了一个人的离开,我们能做的只有
珍惜不期而遇的惊喜。也要接受突如其来的离别。
终将别离的人,就放手吧,把过往的情谊藏在记忆里,然后继续着属于自己的
春秋冬夏。
—————— 一禅心灵庙语
文章目录
创建项目的注意事项
- 首先我们要明白一点就是不存在没有完完全全BUG的项目,就像人无完人一样 我们只能不断地对项目进行优化,减少BUG 的出现,但是我们好像并不能完全的消除所有的BUG
- 而消除bug 的最好的方式就是运行测试+调试
- 而我们在运行测试+调试 的时候,需要我们控制一定的量 ,我们需要把握住这个量 ,这一点是十分重要的,这样有助于我们快速的发现问题,也就是bug 的所在,从而提高我们的效率,减少我们的bug 的出现
- 具体的方法,我个人有以下建议:
- 首先我们需要对项目进行合理的分类,那个类,那个文件实现什么样的功能: 比如:一个类是用于定义什么类型,方法的 ,另一个类是用于什么头main 的,再另外一个类是用于对于什么功能上的实现的 ,合理的分类,可以提高我们项目的可读性,让我们自身不会因为自己随着代码量的增加,对应功能上的增加而导致我们越敲越乱,越写越蒙
- 对于量上我们可以,每实现了两三个 功能就运行测试看看,是否存在错误,如果存在出现了错误,我们可以在一定的量(范围内),快速的找出哪里出现了问题,从而对它进行调试 ,解决问题,而不是,把项目整体写完了或者是已经实现了好几十个功能,才开始运行测试项目,结果一运行,bug,警告 冒出一大坨,我们先不说,让不让人,抓头发,就是找出问题,恐怕都需要不时间上的浪费吧
- 对于代码上的注释,没事多写明白一点,或许有一天,你会感想曾经,那个写了注释的自己或同事
- 对于bug 不要害怕,一点一点的调试看看,找出问题的所在,慢慢来,要有耐心,要明白一点现在bug 写的多,以后bug 跟你说拜拜,现在bug ,不解决,bug 天天对你说 明天见
认识队列
我们在用电脑时有没有经历过,机器有时会出于疑似死机的状态,鼠标点什么似乎都没用,双击任何快捷方式都不动弹,就当你失去耐心,打算 reset时,突然它像酒醒了一样,把你刚才点击的所有操作全部都按顺序执行了一遍,这是因为操作系统在当时可能CPU 一时间忙不过来,等前面的事忙完后,后面多个指令需要通过一个通道输出,按先后次序排队执行造成的结果。
操作系统就是应用了一种数据结构来实现刚才的先进先出的排队功能,这就是队列。
队列: 只允许在一端==(队尾)== 进行插入数据操作,在另一端==(队头)进行删除数据操作的特殊线性表,队列具有先进先出== FIFO(First in First out)入队列:进行插入操作的一端称为队尾 ,出队列: 进行删除操作的一端称为队头
对于使用数组实现队列
如果使用简单的数组实现的话,存在空间上的浪费,如下图
从图上我们可以看到,如果我们出队了,该位置的数据就没有用了,空了,我们又无法在该位置上插入数据,因为队列 只可以在队尾中插入数据 ,所以该空间就浪费了,非要使用的话,我们就需要移动数据,然后面的数据将其覆盖该位置,但是这样移动的时间复杂度为O(n) ,实在不值。当然,我们还是有方法的,就是使用循环队列 的方式,就不会出现大量的空间上的浪费了。具体的我就不多介绍了,因为我们这里的队列是用单链表实现的
单链表实现队列
单链表上实现队列的,要比数组更好,空间上可以大大的节省
代码上队列的实现
这里我们使用的单链表的方式实现队列
创建队列的结构体
这里我们不仅创建一个队列的结构体,也创建一个包含队列 head队头,和tail队尾 的结构体,方便用于后面的传参, 使用typedef 统一定义我们需要存放的数据的类型的别名 ,这样方便我们后面需要存放别的类型的数据的时候,只要修改这一个位置上的类型就可以了,而不需要花费大量的精力和时间。提高了代码的可维护性。
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next; // 存放下一个节点的地址的指针
QDataType data; // 存放该节点的数值
}QNode;
typedef struct Queue
{
QNode* head; // 队头节点
QNode* tail; // 队尾节点
}Queue;
注意了不可以像下面这样使用 typedef 定义结构体
typedef struct QueueNode
{
QNode* next; // 注意不可以这样使用
QDataType data;
}QNode;
因为执行程序语句是从上往下执行的,我们这上面使用后面才生成的QNode ,定义指针是不可以的,QNode* next ,程序有可能会不认识 QNode ,因为它是在后面生成的。
初始化队列
初始化队列,再初始化之前需要判断,该队列是否为空(NULL) ,为空的是无法访问的,这里我们暴力一点吧,使用断言的方式 assert
void QueueInit(Queue* pq)
{
assert(pq); // 断言判断队列是否为 空(NULL)
// 初始化队头,和队尾
pq->head = NULL; // 队头置为 NULL
pq->tail = NULL; // 队尾置为 NULL
}
队尾,入队
对于队列的主要功能,入队存放数据,有如下步骤
- 判断队列是否为空(NULL)
- 使用 malloc 开辟空间(在堆上开辟空间)生成节点,并判断开辟 空间是否成功,成功:赋值初始化,失败:设置错误信息提示,注意 :使用关键字 malloc 函数的时候,该函数是以字节大小开辟空间的,所以建议使用操作符sizeof ,计算大小,就算你知道该结构体的是多少个字节,也建议使用该操作符,因为在不同的位数的系统中的 类型的大小是不一致的,比如;32位系统,int 是占 2 个字节大小,而 64位系统,int 是占 4 个字节大小的,所以不能单单使用数值的方式,而是使用 操作符sizeof ,这样无论是多少位数的系统,都不会错,提高了代码的兼容性
- 判断插入的节点数据是否为第一个数据,是则,队头和队尾都指向这个节点
- 不是,存在多个节点数据,则使用单链表的尾插法 ,关于单链表的尾插法,想要了解的可以移步到 🔜🔜🔜 数据结构 —— 单链表的实现(附有源代码)_ChinaRainbowSea的博客-CSDN博客_数据结构单链表源代码
void QueuePush(Queue* pq, const QDataType x)
{
assert(pq); // 断言,判断队列是否为空(NULL)
// 动态开辟(堆区)空间,创建节点
QNode* newNode = (QNode*)malloc(sizeof(QNode));
// 判断开辟的空间是否成功
if (NULL == newNode)
{
perror("malloc error"); // 开辟空间失败,提示错误信息
exit(-1); // 非正常退出程序
}
else // 否则开辟成功,初始化该节点
{
newNode->data = x; // 为该节点附上值
newNode->next = NULL;
}
// 判断是否为第一个节点,是的话,队尾和队头指向同一个节点
if (NULL == pq->head)
{
pq->head = newNode; // 队头
pq->tail = newNode; // 队尾
}
else // 否则不是第一个节点,多个节点存在,尾插法
{
pq->tail->next = newNode; // 插入节点
pq->tail = newNode; // 更新队尾
}
}
注意 不可以 malloc 为 Queue 结构体,如下, 因为 Queue 并不是队列的结构体,而是包含队头,队尾的结构体,我们真正要使用到的,存放数据的是 QNode 结构体,而且队头和队尾 都是 QNode 类型的 ==(QNode* head; QNode* tail)==使用Queue 类型不符,
Queue* newNode = (Queue*)malloc(sizeof(Queue));
队头,出队
同样出队也是需要判断该队列是否为空的队列,这里我们同样使用断言。对于队头出队,实现的方法就是单链表中的头删法 ,想要了解单链表的头删法的,可以移步到 🔜🔜🔜 数据结构 —— 单链表的实现(附有源代码)_ChinaRainbowSea的博客-CSDN博客_数据结构单链表源代码
队列出队,结构图
void QueuePop(Queue* pq)
{
assert(pq); // 断言,队头不为空(NULL)
QNode* next = pq->head->next; // 保留其队头的下一个节点的地址
free(pq->head); // 释放队头节点的空间
pq->head = next; // 重新指定队头
}
在上述代码中,出队删除中存在一个错误: 当队列中只有一个节点存在时,我们执行上述代码看看,会发现 队头 head->next=NULL ,这个没有问题,但是 队尾 tail 的节点的空间释放了,而却没有置为 NULL,这就会发生可怕的事情了——非法访问 ,因为我们的 队尾tail 的空间已经被free释放了 ,不属于它的了,想要再次通过 队尾tail 的指针访问队尾节点时,该操作就是非法访问了 ,所以我们需要把 队尾tail 置为 NULL ,防止释放了该空间,还可以访问,导致非法访问 。当只有一个节点时,出队,tail = NULL, head = NULL
void QueuePop(Queue* pq)
{
assert(pq); // 断言,队头不为空(NULL)assert(pq)
if (pq->head->next == NULL) // 只有一个节点存在,出队
{
free(pq->head); // 释放队头空间
pq->head = NULL; // 手动置为空(NULL)
pq->tail = NULL;
}
else
{
QNode* next = pq->head->next; // 保留其队头的下一个节点的地址
free(pq->head); // 释放队头节点的空间
pq->head = next; // 重新指定队头
}
}
判断队列是否空
同样断言,队列不为空
对于判断队列是否为空,可以直接通过判断 队头head 是否等于空,对于结果的返回值,这里我们使用布尔值 ,关于C语言中的布尔值 ,大家可以移步到 🔜🔜🔜 你真的了解C语言 if - else 、bool(布尔值)、浮点数损失吗 ?_ChinaRainbowSea的博客-CSDN博客 了解,当结果等于 NULL, 返回 true(数值1) ,不等于,返回false(数值0)
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;
}
取队头数据
同样判断队列不为空,以及还需要判断队列的队头的节点不为 NULL ,因为空的我们又怎么取出数据
都不为空时,取队头数据,直接返回队头存放的数据data 就可以了
QDataType QueueFront(Queue* pq)
{
assert(pq); // 断言,队列不为空
assert(pq->head); // 断言,队头不为空
return pq->head->data; // 取队头中存放的数据
}
写到之这里我们运行一个队列,使用其所有已经实现的功能,看是否存在错误
int main()
{
Queue q; // 定义队列结构体
QueueInit(&q); // 初始化队列
QueuePush(&q, 1); // 队尾,入队
QueuePush(&q, 6); // 队尾,入队
QueuePush(&q, 9); // 队尾,入队
QueuePush(&q, 3); // 队尾,入队
QueuePop(&q); // 队头,出队
QueuePop(&q); // 队头,出队
QueuePush(&q, 5); // 队尾,入队
printf("队列是否为空:%d\n", QueueEmpty(&q)); // 判断队列是否为空
printf("取队头:%d\n", QueueFront(&q)); // 取队头数据
return 0;
}
运行结果
从运行结果上看,没有错误,非常好,我们可以继续实现其队列的其他功能了。
取队尾数据
和取队头数据一样的操作,同样必要的判断操作还是不可以少了的,队列不可为空,队尾的节点不可为空,空了还怎么取数据呢,直接取队尾的数据就可以了data
QDataType QueueBack(Queue* pq)
{
assert(pq); // 断言,队列不为空
assert(pq->tail); // 断言,队尾不为空
return pq->tail->data;
}
队列中的数据个数
同样必要的判断还是少不了,队列不为空,
拷贝一个队头的节点地址,防止循环遍历,移动指针,丢失了整个队列
使用循环遍历,移动指针,计数队列中存在的数据的个数,循环结束,返回其计数结果
int QueueSize(Queue* pq)
{
assert(pq); // 断言,队列不为空
QNode* cur = pq->head; // 拷贝队头节点,代替移动,防止丢失队列
int size = 0; // 计数器
while (cur) // 队头不为空,一直计数下去
{
size++; // 计数
cur = cur->next; // 移动节点
}
// 跳出循环,计数完毕,返回结果
return size;
}
删除队列
判断队列是否为空,循环释放节点空间
拷贝队头节点,代替移动,防止丢失队列
拷贝队头的下一个节点,防止释放空间后,无法找到,下一个节点
void QueueDestory(Queue* pq)
{
QNode* cur = pq->head; // 拷贝队头节点,代替循环遍历移动
while (cur)
{
QNode* next = cur->next; // 保存其下一个节点的地址,防止释放空间,丢失
free(cur); // 释放该节点空间
cur = next; // 移动节点位置
}
pq->head = NULL; // 队头,队尾,置为空(NULL),防止非法访问
pq->tail = NULL;
}
队列的所有基本功能,基本上实现完了,运行实验看看,是否存在 BUG
注意: 和栈一样,取栈顶元素,必须紧接着栈顶出栈,不然无法取到后面的数据,因为栈的特性决定的只有一端可以入栈,出栈操作就是栈顶,遵循先进后出原则,想要了解栈的数据结构,大家可以移步到 🔜🔜🔜 (图文并茂 ,栈的实现 —— C语言_ChinaRainbowSea的博客-CSDN博客
而我们的队列 也是类似的: 取队头数据,必须紧接着队头,出队,才可以取到后面的数据,这是因为队列的特性决定的只有一端队头,才可以出队,另一端队尾才可以入队,遵循先进先出原则
int main()
{
Queue q; // 定义队列结构体
QueueInit(&q); // 初始化队列
QueuePush(&q, 1); // 队尾,入队
QueuePush(&q, 6); // 队尾,入队
QueuePush(&q, 9); // 队尾,入队
QueuePush(&q, 3); // 队尾,入队
QueuePop(&q); // 队头,出队
QueuePop(&q); // 队头,出队
QueuePush(&q, 5); // 队尾,入队
printf("队列是否为空:%d\n", QueueEmpty(&q)); // 判断队列是否为空
printf("取队头:%d\n", QueueFront(&q)); // 取队头数据
printf("队列的数据个数:%d\n", QueueSize(&q)); // 计数队列数据个数
printf("取队尾数据: %d\n", QueueBack(&q)); // 取队尾数据
while (!QueueEmpty(&q)) // 队列不为空循环遍历出队,取数据
{
printf("%d\n", QueueFront(&q)); // 取队头数据
QueuePop(&q); // 队头,出队列
}
printf("\n");
QueueDestory(&q); // 删除队列
return 0;
}
运行结果
队列的完整代码实现
这里我们采用了分布式文件系统设计,下面的代码,大家直接复制,粘贴到 VS 编译器中是可以直接运行的。注意不要漏了,要全部复制粘贴到才行。不然,可能因为缺失一些功能而,无法运行的。
Queue.h
自定义的头文件,用于存放 :其他所需的头文件引入,创建队列的结构体,函数的声明
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h> // 断言
#include<stdbool.h> // 布尔值
#include<stdlib.h> // malloc
typedef int QDataType; // 定义存放队列中的数据类型
typedef struct QueueNode // 创建队列结构体
{
struct QueueNode* next; // 节点指针,注意这里,不可以是 QNode* next ,因为执行顺序的原因,它还没有创建
QDataType data; // 别名上的使用 。存在的数据
}QNode;
typedef struct Queue // 创建队列,队头,队尾的结构体
{
QNode* head; // 队列的首节点
QNode* tail; // 队列的尾节点
}Queue;
extern void QueueInit(Queue* pq); // 初始化队列
extern void QueueDestory(Queue* pq); // 删除队列
extern void QueuePush(Queue* pq, const QDataType x); // 队尾,入队
extern void QueuePop(Queue* pq); // 队头,出队
extern QDataType QueueFront(const Queue* pq); // 取队头数据
extern QDataType QueueBack(const Queue* pq); // 取队尾数据
extern int QueueSize(const Queue* pq); // 计算队列中存在多少元素
extern bool QueueEmpty(const Queue* pq); // 判断队列是否为空
extern void test(); // 队列测试
Queue.c
存放有关队列上的功能的函数的具体实现
//#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
// 初始化队列
void QueueInit(Queue* pq)
{
pq->head = NULL;
pq->tail = NULL;
}
// 删除队列
void QueueDestory(Queue* pq)
{
assert(pq); // 断言,队列不为空(NULL)
QNode* cur = pq->head; // 拷贝队头的地址,用于移动
while (cur)
{
QNode* next = cur->next; // 拷贝下一个节点的地址,用于释放空间,防止丢失整个队列
free(cur); // 释放队头节点的空间
cur = next; // 移动节点
}
// 同时把队头队尾的空间释放掉,并置为 NULL,防止非法访问
free(pq->head);
pq->head = NULL;
free(pq->tail);
pq->tail = NULL;
}
// 队尾,入队
/*
* 两种情况
* 1. 首个数据入队
* 2. 存在多个数据入队
*/
void QueuePush(Queue* pq, const QDataType x)
{
assert(pq); // 队列不为 NULL
QNode* newNode = (QNode*)malloc(sizeof(QNode)); // 注意可以的类型是队列的结构体类
if (NULL == newNode) // 判断开辟空间是否成功
{
perror("创建节点失败");
exit(-1); // 非正常结束程序
}
else // 否则创建节点成功,赋值初始化
{
newNode->data = x;
newNode->next = NULL;
}
if (pq->head == NULL) // 第一个数据,队尾入队
{
pq->head = newNode; // 第一个数据,队尾,队头都是指向同一个节点的地址的
pq->tail = newNode;
}
else // 存在多个数据,队尾入队
{
pq->tail->next = newNode; // 插入节点
pq->tail = newNode; // 插入数据后,重新定义队尾的位置
}
}
/*
// 队头,出队
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->head); // pq->head 首节点不为空
Queue* next = pq->head ->next; // 拷贝保存到首节点下一个节点的地址,防止在释放空间上,丢失了整个队列
free(pq->head); // 释放首节点的空间
pq->head = next; // 重新指向新的首节点
/*但是这里存在一个问题就是
当出队时只有一个节点时,
head 头为空的 队头出队为空的话,没有关系会判断断言
但是队尾 tail, 尾节点释放了空间,但是没有置为 NULL,还会访问该空间,但是这个空间
已经不是它的了,这是非法访问了,非法访问,是会出错的,
所以我们需要将尾节点置为空NULL,防止被访问,
}*/
// 优化
// 队头,出队
void QueuePop(Queue* pq)
{
assert(pq); // 断言 队列不为空(NULL)
assert(pq->head); // pq->head 首节点不为空(NULL)
if (NULL == pq->head->next) // 为空的话,表示只剩下,最后一个尾节点了
{
free(pq->tail);
pq ->head = NULL;
pq->tail = NULL; // 首节点和尾节点都置为NULL
}
else // 存在多个节点拷贝释放
{
QNode* next = pq->head->next; // 拷贝保存到首节点下一个节点的地址,防止在释放空间上,丢失了整个队列
free(pq->head); // 释放首节点的空间
pq->head = next; // 重新指向新的首节点
}
}
// 取队头数据
QDataType QueueFront(const Queue* pq)
{
assert(pq); // 队列不为空(NULL)
assert(pq->head); // 队头不为空(NULL)
return pq->head->data; // 返回队头数据
}
// 取队尾数据
QDataType QueueBack(const Queue* pq)
{
assert(pq); // 队列不为空 (NULL)
assert(pq->tail); // 队尾不为空 (NULL)
return pq->tail->data; // 返回队尾数据
}
// 计算队列中存在多少元素
int QueueSize(const Queue* pq)
{
assert(pq); // 断言 队列不为空(NULL)
assert(pq->head); // 断言 队头也不为空(NULL)
int count = 0; // 计数
QNode* cur = pq->head; // 拷贝队头,代替队头移动,防止丢失整个队列
while (cur) // 队头不为空,
{
count++;
cur = cur->next; // 移动节点
}
return count; // 返回计数结果
}
// 判断队列是否为空队列
bool QueueEmpty(const Queue* pq)
{
assert(pq); // 断言 队列不为空(NULL)
return NULL == pq->head; // 判断一个队列是否为空,判断一下首节点是否为空就可以了,空,返回 true, 非空,返回 false
}
Test.c
main.c 函数的存放处,用于测试队列的功能,是否存在错误
//#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
int main()
{
test();
return 0;
}
// 队列测试
static void test()
{
Queue q; // 定义队列
QueueInit(&q); // 初始化队列
QueuePush(&q, 3); // 队尾,入队
QueuePop(&q); // 队头,出队
QueuePush(&q, 6); // 队尾,入队
QueuePush(&q, 9); // 队尾,入队
QueuePush(&q, 99); // 队尾,入队
QueuePush(&q, 10); // 队尾,入队
QueuePush(&q, 100); // 队尾,入队
printf("%d\n", QueueFront(&q)); // 取队头数据
printf("%d\n", QueueEmpty(&q)); // 判断队列是否为空 NULL
printf("%d\n", QueueBack(&q)); // 取队尾数据
printf("%d\n", QueueSize(&q)); // 计算队列中存在多少个数据
while (!QueueEmpty(&q)) // 判断队列是否为空.队列为空,返回 true,就是数值上的 1,非空,返回false,就是数值上的 0,
{ // 所以这里需要 取非一下
printf("%d ", QueueFront(&q)); // 取队头数据
QueuePop(&q); // 队头,出队
/*
* 注意队列和栈一样,栈是,取栈顶元素,后必须紧接着栈顶出栈,不然你是无法取到后面的数据的
* 而队列也是一样的,队列是,取队头数据,后必须紧接着队头出队,不然你是无法取到后面的数据的
*/
}
printf("\n");
QueueDestory(&q); // 删除队列
}
以上便是队列的全部内容了,
最后:
限于自身水平,其中存在的错误,希望大家给予指教,韩信点兵——多多益善,谢谢大家,后会有期,江湖再见!