话不多说,先上整体无头单向不循环单链表实现的代码给大家
single_linked_list.h
#include //标准输入输出函数等所需要的头文件
#include //断言所需要的头文件
#include //扩容和销毁等所需要的头文件
//类型重命名,方便后续修改类型存储数据
typedef int SLLDataType;
//定义单链表结点结构体,最好重命名方便使用
typedef struct single_linked_list_node
{
SLLDataType data; //用于存储数据
struct SLLNode* next; //用于连接下个结点
}SLLNode; //将单链表重命名
void print(SLLNode* phead); //打印单链表
SLLNode* find(SLLNode* phead, SLLDataType x); //查找单链表指定数据的结点位置
void push_head(SLLNode** pphead, SLLDataType x); //单链表头插
void pop_head(SLLNode** pphead); //单链表头删
void push_back(SLLNode** pphead, SLLDataType x); //单链表尾插
void pop_back(SLLNode** pphead); //单链表尾删
void push_pos_after(SLLNode** pphead, SLLNode* pos, SLLDataType x); //单链表任意位置插入(插入目标位置之后)
void push_pos_front(SLLNode** pphead, SLLNode* pos, SLLDataType x); //单链表任意位置插入(插入目标位置之前)
void pop_pos_after(SLLNode* pos); //单链表任意位置删除(删除目标位置之后)
void pop_pos(SLLNode** pphead, SLLNode* pos); //单链表任意位置删除(删除目标位置)
void destroy(SLLNode** pphead); //单链表的销毁
single_linked_list.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "single_linked_list.h" //先包含单链表的头文件
SLLNode* creat_node(SLLDataType x) //创建单链表结点
{
SLLNode* new_node = (SLLNode*)malloc(sizeof(SLLNode));
//谨慎判断下,是否内存开辟成功了
if (new_node == NULL)
{
printf("malloc invalid!\n");
exit(-1); //若开辟失败,终止程序
}
new_node->data = x; //将目标数据存储入单链表新结点里面
new_node->next = NULL; //默认新结点连接的下个结点为空指针
return new_node; //将结点返回去,当然这里你不写也可以,不影响
}
void print(SLLNode* phead) //打印单链表
{
SLLNode* cur = phead;
while (cur) //开始遍历单链表
{
printf("%d->", cur->data); //逐个打印单链表里面的数据
cur = cur->next; //切换到下个结点的位置
}
printf("NULL\n"); //单链表的结束标志是末尾连接空指针
}
SLLNode* find(SLLNode* phead, SLLDataType x) //查找单链表指定数据的结点位置
{
SLLNode* cur = phead;
while (cur) //开始遍历单链表
{
if (cur->data == x) //检测该结点里面是否存储了目标数据
{
return cur; //若存储的是目标数据就将该结点返回去
}
else
{
cur = cur->next; //若不是目标数据就切换到下个结点接着寻找
}
}
//当整个单链表都遍历查找完还没有找到目标数据的话,说明目标数据并未存储在单链表里面
return NULL; //返回空指针,代表没有找到目标数据所在的结点
}
void push_head(SLLNode** pphead, SLLDataType x) //单链表头插
{
assert(pphead); //判断单链表的头结点是否创建成功了
SLLNode* new_node = creat_node(x); //调用创建新结点的函数
new_node->next = *pphead;
*pphead = new_node;
}
void pop_head(SLLNode** pphead) //单链表头删
{
assert(*pphead);
SLLNode* head_next = (*pphead)->next;
free(*pphead);
*pphead = head_next;
}
void push_back(SLLNode** pphead, SLLDataType x) //单链表尾插
{
assert(pphead);
SLLNode* new_node = creat_node(x);
if (*pphead == NULL)
{
*pphead = new_node;
}
else
{
SLLNode* tail_node = *pphead;
while (tail_node->next != NULL)
{
tail_node = tail_node->next;
}
tail_node->next = new_node;
}
}
void pop_back(SLLNode** pphead) //单链表尾删
{
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLLNode* tail_node = *pphead;
SLLNode* previous_tail = NULL;
while (tail_node->next != NULL)
{
previous_tail = tail_node;
tail_node = tail_node->next;
}
previous_tail->next = NULL;
free(tail_node);
tail_node = NULL;
}
}
void push_pos_after(SLLNode** pphead, SLLNode* pos, SLLDataType x) //单链表任意位置插入(插入目标位置之后)
{
assert(pphead);
assert(pos);
SLLNode* new_node = creat_node(x);
//插入分为两种情况:分别是刚开始并未存储数据和已经存储了数据
//第一种情况
if (*pphead == NULL && *pphead == pos)
{
pos = new_node;
}
//第二种情况
else
{
SLLNode* pos_next = pos->next;
pos->next = new_node;
new_node->next = pos_next;
}
}
void push_pos_front(SLLNode** pphead, SLLNode* pos, SLLDataType x) //单链表任意位置插入(插入目标位置之前)
{
assert(pphead);
assert(pos);
SLLNode* new_node = creat_node(x);
//分为两种情况:目标位置就是第一个结点的位置和不是第一个结点的位置
//第一种情况
if (*pphead == pos)
{
/*new_node->next = *pphead;
*pphead = new_node;*/ //你也可以再写类似于头插的方式插入,但是不推荐
push_head(pphead, x); //推荐直接调用头插的接口,代码复用,更加简洁、高效
}
//第二种情况
else
{
SLLNode* previous_pos = NULL;
SLLNode* cur = *pphead;
while (cur != pos) //遍历循环,终止条件为找到目标位置结点
{
previous_pos = cur; //找到目标位置的前一个结点
cur = cur->next; //切换到下个结点接着寻找
}
previous_pos->next = new_node; //目标位置结点的前一个结点连接的下一个位置为新结点
new_node->next = pos; //新结点连接的下一个位置为目标位置结点
}
}
void pop_pos_after(SLLNode** pphead, SLLNode* pos) //单链表任意位置删除(删除目标位置之后)
{
assert(*pphead);
assert(pos);
//分为两种情况:分别是删除只存储了一个数据和删除已经存储了多个数据
//第一种情况
if (pos == *pphead && (*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//第二种情况
else
{
SLLNode* pos_next = pos->next;
SLLNode* pos_next_next = pos_next->next;
pos->next = pos_next_next;
free(pos_next);
pos_next = NULL;
}
}
void pop_pos(SLLNode** pphead, SLLNode* pos) //单链表任意位置删除(删除目标位置)
{
assert(*pphead);
assert(pos);
//分为两种情况:分别是删除只存储了一个数据和删除已经存储了多个数据
//第一种情况
if (*pphead == pos)
{
/*SLLNode* head_next = (*pphead)->next;
free(*pphead);
*pphead = head_next;*/
pop_head(pphead); //还是老规矩这边直接建议调用头删的函数接口
}
//第二种情况
else
{
SLLNode* cur = *pphead;
SLLNode* front_pos = NULL;
while (cur->next != pos)
{
front_pos = cur;
cur = cur->next;
}
front_pos->next = pos->next;
free(pos);
pos = NULL;
}
}
void destroy(SLLNode** pphead) //单链表的销毁
{
SLLNode* cur = *pphead;
SLLNode* temp = NULL;
while (cur)
{
temp = cur->next;
free(cur);
cur = temp;
}
*pphead = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "single_linked_list.h" //先包含单链表的头文件
//头插头删测试
void test1()
{
SLLNode* plist = NULL;
push_head(&plist, 1);
push_head(&plist, 2);
push_head(&plist, 3);
pop_head(&plist);
pop_head(&plist);
pop_head(&plist);
pop_head(&plist);
print(plist);
}
//尾插尾删测试
void test2()
{
SLLNode* plist = NULL;
push_back(&plist, 1);
push_back(&plist, 2);
push_back(&plist, 3);
push_back(&plist, 1);
push_back(&plist, 2);
push_back(&plist, 3);
pop_back(&plist);
pop_back(&plist);
pop_back(&plist);
pop_back(&plist);
pop_back(&plist);
pop_back(&plist);
pop_back(&plist);
print(plist);
}
//任意位置插入(目标位置之后)
void test3()
{
SLLNode* plist = NULL;
push_back(&plist, 1); //目前已经放入了一个数据1,在第一个结点
//任意位置插入(目标位置之后)需要配合查找函数使用
SLLNode* flag1 = NULL; //设置临时变量结点,用来配合查找目标结点
flag1 = find(plist, 1); //查找第一个1数据的结点,在其后接着插入一个1
if (flag1 != NULL)
{
push_pos_after(&plist, flag1, 1); //完成插入
}
push_back(&plist, 1);
push_back(&plist, 1);
//此时目前已经插入了有4个1数据的结点了
//假定我们要在第二个1所在的结点后插入数据8
//如何配合查找函数将其插入呢?
//代码如下:
SLLNode* flag2 = NULL;
flag2 = find(plist, 1); //拿到第一个存储1数据的结点
int i = 0;
while (flag2)
{
i++;
if (i == 2)
{
push_pos_after(&plist, flag2, 8); //完成插入
break;
}
flag2 = flag2->next;
}
//我以此为例,告诉大家如何制定在哪个1后面插入数据
//只需要大家灵活控制查找函数,查到这个存储1的结点以后
//下次查找的时候就从当前存储1这个数据结点的下个结点开始进行查找
//以此类推,后不演示了
print(plist);
}
//任意位置插入(目标位置之前)
void test4()
{
SLLNode* plist = NULL;
push_back(&plist, 1);
push_back(&plist, 1);
SLLNode* flag1 = NULL; //设置临时变量结点,用来配合查找目标结点
flag1 = find(plist, 1); //查找第一个1数据的结点,在其前接着插入一个9
if (flag1 != NULL)
{
push_pos_front(&plist, flag1, 9);//完成插入
}
print(plist);
}
//任意位置删除(目标位置之后)
void test5()
{
SLLNode* plist = NULL;
push_back(&plist, 1);
/*push_back(&plist, 2);
push_back(&plist, 3);*/
SLLNode* flag1 = NULL; //设置临时变量结点,用来配合查找目标结点
flag1 = find(plist, 1); //查找第一个1数据的结点,在其前接着插入一个9
if (flag1 != NULL)
{
pop_pos_after(&plist, flag1);//完成删除
}
print(plist);
}
//任意位置删除(目标位置)
void test6()
{
SLLNode* plist = NULL;
push_back(&plist, 1);
push_back(&plist, 2);
push_back(&plist, 3);
SLLNode* flag1 = NULL; //设置临时变量结点,用来配合查找目标结点
flag1 = find(plist, 1); //查找第一个1数据的结点,在其前接着插入一个9
if (flag1 != NULL)
{
pop_pos(&plist, flag1);//完成删除
}
print(plist);
}
int main()
{
//test1(); //头插头删测试
//test2(); //尾插尾删测试
//test3(); //任意位置插入(目标位置之后)
//test4(); //任意位置插入(目标位置之前)
//test5(); //任意位置删除(目标位置之后)
test6(); //任意位置删除(目标位置)
return 0;
}
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
分类:实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向
2. 带头或者不带头
3. 循环或者非循环
其中最经典的为无头单向非循环链表和带头双向循环链表
本篇文章主要讲述的就是无头单向非循环链表的实现,后续会继续更新带头双向循环链表的,我们单链表所主要实现的功能和顺序表是一样的,本质上就是数据的增删查改,只不过顺序表是物理上连续的存储空间,单链表是物理上不连续的存储空间。【等后续双链表博客更新完成以后,我会在文章末尾专门对比阐述顺序结构存储(以经典的顺序表为例)和链式结构存储(以经典的双链表为例)的优缺点】
在开始实现我们单链表的功能之前,我首先需要声明的是,由于大部分的OJ题(不论是牛客网,还是leedcode),都是以无头单链表为主,也就是说通常我们单链表都是不带头的,学校的单链表实现为了简单通常都是有带头结点的,但是在我们实际应用中基本都是无头的单链表居多,而我实现的是一个比较经典常用的单链表结构,为无头单向不循环的单链表。
在我们的头文件(single_linked_list.h)中,首先定义一个单链表结点的结构体,内容包括要存储的数据(数据类型可以typedef下,方便修改为其他类型存储数据)和同类型的指针变量(用于连接下个单链表结点),紧接着是定义单链表对应的各个接口,头尾位置的插入删除,任意位置的插入删除,以及打印和销毁单链表等等。
代码实现如下图所示:
single_linked_list.c
在我们的源文件(single_linked_list.c)中,将分别具体实现这些接口的功能。
如下所示:
思路:
考虑为了方便后续观察我们单链表的实现情况,我们首先先实现单链表的打印显示的函数功能。
函数形参我们使用一级指针即可,因为不涉及需要改变指针内容。
首先用一个同类型的单链表结点指针来接收形参指针,然后使用一个while循环【循环结束条件为下个指针连接为空指针,原因在于单链表是以空指针为末尾结束(我们想的是循环结束的条件,但是我们写的是循环继续的条件)】来逐个打印单链表存储的数据内容,每执行一次while循环就将单链表当前结点的位置移动到下个结点的位置,最后while循环结束以后,最好再打印一个NULL和换行以示区分。
思路:
考虑到我们的插入操作(不论是头插、尾插、还是任意位置的插入操作)都需要创建一个新的结点才能将数据插入进去,所以我们单独把创建一个新结点提取出来作为一个函数,后续相应插入操作函数的插入直接调用该函数方便直接创建单链表的新结点。
传入该函数的参数即是要存储的数据,调用malloc函数创建一个同类型的单链表结点指针,开辟内存完成以后可以检查一下是否开辟内存失败,若失败则调用exit函数退出。(虽然很难出现失败的情况,但是严格一点检查是比较好的编程习惯)
开辟内存成功的话,就将数据存储进去新结点,同时将新结点连接下个指针指向空指针,再将我们的新结点返回去即可。
注:由于接下来的任意位置的插入和任意位置的删除操作我采用的都是利用指针来定位确定位置,也就是说得确定你要插入或者删除的地址位置才能进行对应的插入或者删除操作,所以得需要配合一个查找函数来使用,确定你的目标位置。
思路:
这里我们可以采用的是一级指针传参的方式,因为不需要变动单链表的头结点。
首先还是用一个同类型的单链表结点指针来接收形参指针,然后引用一个while循环遍历寻找目标位置,终止条件就是该结点为空指针时,在执行循环的过程的当中,如果我们找到了当前结点所存储了我们需要的目标数据(也就是我们要找的目标位置),我们就将这个目标位置返回去,不然就换到下个位置接着寻找,循环结束还没有找到就返回来空指针,代表找不到目标位置。
思路:
为了防止以外情况发生,我们首先需要断言下该二级指针是否为空,若为空说明单链表并没有开始创建,无法开始进行我们如下的操作。
然后开始创建我们的新结点将我们的存储的数据放入新结点里面。
首先将新结点连接的下个位置由原先的空指针变为单链表的起始位置,也就是第一个单链表结点的位置。
再将原先指向单链表第一个位置的结点指针改为指向新结点的位置即可完成头插操作。
代码实现如下图所示:
注:由于后续单链表的许多操作我都是采用的是二级指针的方式来解决第一次插入的问题,【从第二次插入开始就不需要再像第一次插入那么麻烦了,可以直接从当前结点通过连接的下个结点找到位置】因此我这里举个例子来方便大家理解为何需要传递二级指针。
例如:假设你定义了两个整形变量a和b并初始化为2和3,我们想通过一个交换函数将a和b的值进行交换,那么我们是不是应该就进行传址传递呢?答案是肯定的,我们需要传a和b的地址进去才能更改他们本身的值,所以我们形参接收是int*类型,也就是一级指针。(这里我要说明下,在C语言中指针本质上就是地址,你也可以理解为是一种类型,专门用来定义存放地址的类型)
a和b变量是int类型,传址过去,所以函数形参接收要用一级指针
那么单链表的结点变量本身就是是int*类型,传址过去,那么函数形参接收是不是就得用二级指针呢?答案是肯定的,必须得用二级指针来接收。
思路:
分为两种情况,分别是一个结点都没有你还要头删,和有结点你想要头删。
第一种情况:
没有结点的时候你想要头删,那就跟尾删一样的道理,直接用assert断言报错。
第二种情况:
有结点的时候你想要头删,那我们首先得定义一个临时的单链表结点变量是用来记录当前单链表结点的下个位置,然后我们就能直接调用free函数将当前单链表结点释放掉,然后将原先指向当前结点的指针指向前面已经记录好的临时变量位置,即可完成头删操作。
先声明下,考虑到第一次插入的特殊情况,由于我用C语言实现该单链表,所以我采用的是传二级指针的方式来解决,当然,你也能通过函数返回值的方式解决这个问题,在C++等面向对象的语言中你还能采用引用的方式解决,解决方法多种多样,不限于此。
思路:
为了防止意外情况发生,我们首先需要断言下该二级指针是否为空,若为空说明单链表并没有开始创建,无法开始进行我们如下的操作。
然后开始创建我们的新结点将我们的存储的数据放入新结点里面。
我们的插入操作(不论是头插、尾插,还是任意位置的插入操作)分为两种情况,一种是第一次插入操作(原先什么数据都没有),另外一种是已经进行过了第一次插入操作(已经有数据了)。
当是第一种情况进行尾插操作时,我们可以直接将刚创建好的单链表结点(我们创建的单链表结点初始化为空指针,这样子就不需要再创建单链表初始化的结点了)直接更改为新结点就完成尾插操作了。
当是第二种情况进行尾插操作时,我们需要进行找尾操作(尾部就是空指针),所以我们需要进行遍历一遍单链表进行找尾,跟遍历打印单链表函数类似,只不过注意的是这里我们传的是二级指针而不是一级指针,我们需要的是传址传递而不是传值传递,所以我们定义一个一个同类型的单链表结点指针指向的是第一个单链表结点的位置,而不是存储单链表第一个结点的位置,这两个是有非常巨大的差别的!
找到尾部前一个结点的位置以后,将前一个结点连接新结点即可完成尾插操作。
思路:
跟单链表第一次插入同理,单链表的第一次删除也是特殊情况,我依旧统一全部采用传二级指针方法处理。
我们单链表的尾删分为三种情况,分别是:第一种情况:一个结点都没有你还调用尾删操作;第二种情况:只有一个结点的情况下你调用尾删操作;第三种情况:一个以上的结点你调用尾删操作。
先来说第一种情况:
就是一个结点都没有的时候你还想删除,分别有两种方法可以来处理,我个人比较推荐用粗暴的方式(使用assert断言当前结点是否为空),你也可以使用温柔的方式(if语句判断),为什么推荐使用粗暴的方式,原因在于当你在公司工作的时候,需要维护几十万行以上的代码,用assert断言可以让你立即知道是哪里错了,而使用if语句判断找错误相对麻烦一些,秉承着怎么方便怎么来的原则,所以使用粗暴方式,不管是上文还是下文,后续我就不说明为何推荐使用assert断言而不是使用if语句判断了。
第二种情况:
就是当只有一个结点的时候,我们采用if语句判断当前结点连接的下个位置是不是空指针,如果是就代表只有一个结点,否则的话就是一个以上结点,则是第三种情况。
当我们得知只有一个结点的时候,我们可以直接使用free函数将其释放这个结点内存空间,然后再将我们当前结点置空就完成尾删操作了。
第三种情况:
当我们得知有一个以上结点的时候,也就是第二种情况if语句判断失败而进入的else语句,此时就为第三种情况。
我们要尾删数据,势必得知道尾部结点的前一个位置才能进行删除操作,否则你直接进行尾删操作,尾部前一个结点没有把连接的下个位置变为空指针,然而你又将尾部结点释放了,这就属于非法访问内存泄露了(举个例子:那个房子不是你的,你还要强行进去住),这点你得理解。所以我们得定义一个临时的结点变量用来记录尾部结点的前一个位置,然后剩下就是找尾和找尾部前一个结点的位置了,这个跟前面的尾插操作找尾类似,这里我就不过多叙述。
找到这两个关键结点以后,我们就将尾部结点调用free函数释放掉,再将尾部结点置空(这里比较规范写法,释放万内存以后就将内存位置置空),最后再把尾部前一个结点连接的下个位置更改为空指针即可完成尾删操作。
思路:
我将单链表任意位置插入分为了两种方式来实现,分别是目标位置的下一个位置实现任意位置插入的操作(实现起来较为简单)和目标位置的前一个位置实现任意位置插入的操作(实现起来较为复杂)。注:两种方式首先都是需要配合我们的单链表查找函数才能实现得了。
第一种方式:目标位置的下一个位置实现任意位置插入的操作
拿到了我们目标位置以后先判断该指针是否为空指针,如果为空指针代表说明查找失败了,不存在目标位置,还是老规矩采用assert函数断言报错。
当确定目标位置存在时,我们才进行接下来的操作,跟之前的头插和尾插操作一样,都需要先定义开辟好一个新结点的内存空间。
然后新结点的下一个位置指针指向目标位置的下一个位置,再将目标位置的下一个位置指针指向新结点就完成任意位置插入操作了。
代码实现如下图所示:
第二种方式:目标位置的前一个位置实现任意位置插入的操作
还是需要借助二级指针传参,该有的粗暴断言方式还是不能少。
此时任意位置的插入分为两种情况讨论,分别是目标位置就是当前单链表第一个结点的位置和不是单链表第一个结点的位置。
第一种情况:
就是单链表头插的方式,你可以直接调用单链表头插函数完成操作。
第二种情况:
我们需要先找到目标位置的前一个结点的位置,套路跟前面单链表尾插找尾的方法类似,只不过这次是找头,不过多叙述,找到前一个位置以后,将前一个位置连接的下一个位置的单链表结点更改为新结点,将新结点的下个位置连接为目标位置结点即可完成操作。
逻辑结构图如下(只画了第二种情况的,第一种情况参照单链表的头插):
思路:
我将单链表任意位置删除分为了两种方式来实现,分别是直接删除目标位置和删除目标位置的下一个结点位置。注:跟任意位置插入操作同理,都需要配合我们的单链表查找函数才能实现。
第一种方式:直接删除目标位置
依旧是二级指针传参。
首先还是老规矩,该断言检查报错还是别忘记了。
然后呢跟单链表任意位置插入一样,分为两种情况讨论,分别是目标位置就是当前单链表第一个结点的位置和不是单链表第一个结点的位置。
第一种情况:
就是单链表头删的方式,你可以直接调用单链表头删函数完成操作。
第二种情况:
我们还是需要先找到目标位置的前一个结点的位置,套路跟前面的单链表任意位置插入操作的第二种方式的第二种情况类似,找到目标位置前一个结点以后,将前一个结点连接的下个位置直接变为目标位置连接的下个位置,然后调用free函数将目标位置销毁归还给系统,最后比较严谨的话最好将目标位置结点置空,当然这里不置空也不影响什么。
逻辑结构图(还是一样,只画了第二种情况的,第一种情况参照单链表的头删)如下:
代码实现如下图所示:
第二种方式:删除目标位置的下一个结点位置
还是老规矩,二级指针传参,该有的断言报错不能少。
首先先定义一个同类型的结点临时变量记录目标位置的下一个结点先,接着再将目标位置的下一个结点变为临时变量的下一个结点,最后再调用free函数将目标位置的下一个结点的内存空间归还给操作系统,即可完成删除操作,free后的指针通常都是需要置空的,不过这里老规矩置空不置空都不影响。
思路:
老规矩,该有的断言不能少。
根据单链表的特性,不连续空间的存储数据,所以要删除单链表结点之前,我们得需要提前拿到当前结点的下个结点的位置,然后才能开始删除当前结点,所以还是老套路用一个临时变量记录下个结点的位置,用while循环调用free函数来逐步删除单链表结点,循环条件还是当结点为空就结束,然后就是每删除当前结点就换下个结点接着删除就好了,前提是别忘记提前记录下个结点的位置了。
test.c
在这一部分就是我们单链表函数各个接口的实现测试是否合格,还是按照我说的老规矩,建议大家边写边测试,这样最好,一旦发现错误就能知道是哪里错了,而不是一次性全部写完再来测试单链表的代码实现,这样子很难发现具体是哪个位置的错误,非常地耽误时间。
任意位置删除(目标位置)测试代码演示如下:
在main函数里面逐个调用接口用来测试实现:
楼主不才,不喜勿喷,若有错误或需要改进的地方,非常感谢你的指出,我会积极学习采纳。谢谢家人们一直以来的支持和鼓励,我会继续努力再接再励创作出更多优质的文章来回报家人们的。编程爱好的xdm,若有编程学习方面的问题可以私信我一同探讨(我尽力帮),毕竟“众人拾柴火焰高”,大家一起交流学习,共同进步!