DS:单链表的实现

欢迎各位来到 Harper.Lee 的编程学习小世界!

博主主页传送门:Harper.Lee的博客

我将在这里分享我的学习过程等心得

创作不易,码字不易,兄弟们养成先赞后看的好习惯哦!

想一同进步的uu,可以来后来找我哦! 

      在前面的文章中,博主详细地介绍了顺序表的实现方法及其书写过程,大家可以跳转去学习:DS:顺序表的实现

目录

一、顺序表存在的问题

二、链表的概念及结构

三、单链表节点结构体的创建

四、单链表的实现

4.1 创建单链表的节点

4.2 新节点的申请(4.1的升级)

4.3 链表打印函数 

五、增加

5.1 尾插

5.2 头插

5.3 指定位置之前插入

5.4 指定位置之后插入

六、删除

6.1 尾删

6.2 头删

6.3 删除指定位置之前的节点

6.4 删除指定位置之后的节点

6.5 销毁链表

七、查找

八、总结



一、顺序表存在的问题

1. 在顺序表的中间/头部插入数据,会涉及到数据的多次移动,效率不高;

2. 增容需要申请空间、拷贝数据、释放旧空间,会有不小心的消耗;

3. 增容一般是呈1.5倍、2倍形式增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入5个数据,那么就会浪费95个空间。

       总的来说,顺序表存在以下几个问题:中间 /头部插入效率低下、增容降低运行效率、增容造成空间浪费。为了解决这几个问题,提出了一个新的数据结构---链表

二、链表的概念及结构

        概念:链表也是顺序表的一种,在逻辑结构上是线性的,在物理结构上是非线性的,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。如,int a =10 ; float f = 0.1 ;变量 a 和 f 的物理空间不一定是连续的,这取决于OS操作系统分配空间的方法。

        链表就类似于现实生活中的火车(如下图),火车是由一节一节的车厢和一个火车头组成的,车厢之间是通过可拆可卸的钩子挂在一起的,每节车厢都是独立存在的。当处于旅游旺季时,增加车厢;如果出现候补,处于淡季时,就可以通过减少车厢来减少空间的浪费。当车厢使用数量需要变化时,只需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。

        类似地,链表是由一个一个的节点(结点也一样,没有区别)组成。由之前的知识可知,动态申请的空间在堆上,链表在物理结构虽然不是线性的,但它可以通过节点联系起来,就像一条弯曲的链子,可以通过拉直让物理结构变成想象的线性结构(就好比弯曲的链子拉直)。由下图可知,链表中的节点存储的内容为两个部分:可以指向下一个节点的地址(pointer)以及本节点的存储数据(data)。此外, 链表的每个节点都是独立申请的空间,就像火车的每节车厢都是独立的一样。

       ⻋厢是独立存在的,且每节车厢都有车门。想象⼀下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下,如何从车头走到车尾?
最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。

       图中指针变量plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希望plist“指向”第⼆个节点时,只需要修改plist保存的内容为0x0012FFA0。  链表中每个节点都是独⽴申请的(即需要插入数据时才去申请⼀块节点的空间,而顺序表是要动态申请一块连续的空间),我们需要通过指针变量来保存下⼀个节点位置,这样就能能从当前节点找到下⼀个节点,将整个链表串联起来。

三、单链表节点结构体的创建

        单链表(single linked list)是由一个一个的节点组成,节点又由指针(指向下一个结点的指针)和数据组成。如果要定义节点,只需要定义链表节点的结构即可,因此需要用到结构体指针。

//本项目相关可能用到的头文件
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>//定义节点的结构
//data+指向下一个节点的指针typedef int SLTDataType;//类型用SLDataType来表示,便于后续操作对数据类型额更改typedef struct SListNode
{SLTDataType data;//当前节点存储的数据struct SListNode* next;//next:(指向下一个结点的结构体指针变量)
}SLTNode;//重命名一个简单的名字

        当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。

        当我们想要从第⼀个节点⾛到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下⼀个节点的钥匙)就可以了。 

四、单链表的实现

4.1 创建单链表的节点

	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));//动态申请空间的函数,malloc、caoolc、realloc等函数//链表中不涉及增容,所以使用malloc?   返回值void*强制类型转换SLTNode*node1->data = 1;//第一个节点的指针指向那个下一个节点的地址,所以先创建第二个节点,便于前面的node1找到(存储)后面的节点的地址SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));node2->data = 2;SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));node3->data = 3;SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));node4->data = 4; //最后一个节点

       现在虽然创建好了一个个的节点,但是这些节点之间相互独立,没有联系在一起。因此,使用next指针变量将两个节点之间进行连接。 

       链接节点:(指针)

//使用指针将四个节点通过个next连接起来
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;//最后一个节点,不需要存储地址了,没有节点了

 为什么说来链表节点的创建用malloc开辟动态内存空间?

       之前在顺序表里,我们使用了realloc开辟动态内存空间,是因为顺序表需要进行增容操作,动态申请连续的空间。链表是由一个一个的节点组成,只需要按照节点进行动态申请空间即可,不涉及增容操作,因此节点的空间只需一次开辟一次内存内存就可以了,而且它也没有连续存放的要求,不需要在原有的空间基础上申请,因此使用malloc就可以了。

4.2 新节点的申请(4.1的升级)

        在对单链表进行操作时,我们会涉及到尾插、头插、指定位置插入等的操作,并且还要为新节点动态申请一块空间,所以对新节点申请这一功能进行函数封装,以便于直接使用,无需像4.1 那样一个一个的手动实现。链表初始时多少空链表,我们在链表里进行多次增加数据

//当插入节点时,都在重复判断空间是否充足,然后再malloc申请空间,
// 因此此功能单独提取出来
SLTNode* SLTBuyNode(SLTDataType x)//SLTNode*为返回数据的类型
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){//空间申请失败!perror("malloc fail!");exit(1);//异常退出,使用非零}newnode->data = x;newnode->next = NULL;
}

4.3 链表打印函数 

代码如下:

//链表的打印:
void SLTPrint(SLTNode* phead)//链表的头节点
{SLTNode* pcur = phead;while (pcur)//等价于pcur != NULL{printf("%d->", pcur->data);pcur = pcur->next;//两个节点通过next打印输出5}printf("NULL\n");//此时pcur为NULL,不需要置空了
}

调用该链表打印函数:

       分析代码运行过程:plist作为函数参数,传给函数中的行参phead,phead得到plist存储的数据即第一个节点的地址,也就是phead得到链表第一个节点的地址。pcur作为二级指针,接受了phead存储的值(一级指针),也得到第一个节点的地址。地址不为空,就开始进行打印操作,pcur->pcur.next; 进行,pcur的next存储的内容变成下一个节点地址。判断pcur是否为NULL,若pcur存储NULL,循环条件为假,循环结束,打印结束。(运行结果:1->2->3->4->NULL)     

  1、为什么不直接使用node1,而要多创建一个结构体指针变量plist呢?

        调用打印函数时,多创建了一个结构体指针变量plist指向第一个节点,虽然有点多此一举,但是可以不用改变链表的数据

2、 这里其实不需要传地址,因为打印函数只不过是展示,并没有对链表的数据进行处理,值传递也可以(SLTNode*phead),如果我们是为了保持接口一致性,可以使用二级指针接收参数。

五、增加

接口定义:

void SLTPrint(SLTNode* phead);//从链表的头phead开始打印//链表的接口定义,
//尾插
void SLTPushBack(SLTNode**  pphead, SLTDataType x);//pphead头节点
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);

5.1 尾插

图解分析:

       尾插需要找到尾节点,再将尾节点和新节点连接起来。在代码中,我们定义了一个新的指针ptail,指向第一个节点的地址,让它进行遍历,指导找到尾节点。然后让尾节点的next指针指向新的节点。但如果链表是一个空链表,就没有尾节点了,所以要考虑链表是否为空的情况。

代码如下:

//尾插
//要先抓到尾节点,再将尾节点和新节点链接起来
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);//*pphead可以为空//*pphead是指向第一个节点的指针SLTNode* newnode = SLTBuyNode(x);//建立一个新节点if (*pphead == NULL)//判断第一个节点是否为空{//空链表:申请一个新的节点,让phead指向这个新的节点,此时,链表就变成只有一个节点的链表*pphead = newnode;//}else{//非空链表:找尾节点,从头开始找SLTNode* ptail = *pphead;//定义ptail先指向头节点,然后从头往后(循环)开始找尾巴while (ptail->next)//->表示指针的解引用{ptail = ptail->next;//指向下一个节点,继续往下走}//此时ptail指向的就是尾节点//连接尾节点之前需要一个新节点,在进行新节点的创建ptail->next = newnode;}

注意:

1、为什么要创建一个结构体指针ptail去接收*pphead,而不直接使用*pphead?

      用ptail记录头节点,用它通过遍历寻找尾节点 (先找尾节点,再连接尾节点和新节点)

      *pphead是指向头节点的一级指针,我们是通过二级指针去接收该一级指针的地址,所以*pphead是会被改变的,如果我们在寻找尾结点的时候直接用*pphead,虽然也可以找到尾结点,但是头结点也会因此而丢失,所以我们在利用头结点去遍历链表时,一定要创建一个临时变量去接收头节点再去遍历。

2、 为什么传入的参数要使用SLTNode** pphead(二级指针)?

运行结果如下图:

        调用函数时,plist是一个指向第一个节点的一级指针变量,将它的变量名直接作为参数传给行参,其实传过去的是它的值,而不是它的地址,(调试发现:行参的值没有改变,说明是传值调用而这里需要的是地址即传址调用而非传值调用。此外,plist是一级指针,因此行参接收 &plist 二级指针,类型就为二级指针。

     上图中,左边是实参三种形式,右边是行参三种形式。

3、为什么使用assert(pphead)?

        如果调用尾插函数时,程序猿给实参传入了一个NULL,行参接收到后就会对它进行解引用,程序会出错。因此加入断言,断言错误就会报错。

4、为什么还要判断链表是否为空链表?

       如果链表为空链表,则把新创建的节点作为该空链表的唯一节点;如果链表不是空链表,则需要找到尾节点,然后连接新节点和尾节点,就完成了尾插。

       最开始创建的链表是一个空链表 ,phead指向的就是一个空链表,先申请一个新节点newnode时,定义 ptail指向头节点phead,所以ptail为空,开始找尾节点。ptail->next中->操作符相当于指针解引用,对空指针进行解引用,代码就会报错。因此,在代码里要增加判断空链表这一情况:空链表不需要找尾节点,只需要申请一个新的尾节点,phead原本指向的是NULL,现在让phead指向新创建的节点。简而言之:新创立的节点就是phead指向的头节点,就是该空链表唯一的节点。

5、为什么while循环中的判断条件是ptail->next != NULL,而不是ptail != NULL?

       while循环是为了找到尾节点,如果循环条件是ptail != NULL,当循环条件结束时,ptail指向的节点不是尾节点,没有找到尾节点,则目的没有达到;如果循环条件是ptail->next != NULL,那么条件结束时,ptail指向的next就是尾节点。

5.2 头插

链表不为空的情况图解:

链表为空的情况:

        

        一般情况下,我们只需要让newnode的next指针指向原来的头节点,再让newnode成为新的头节点,当链表为空时,插入的newnode就是新的头节点,所以不需要分开讨论。

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);//判断二级指针接收的内容是否为空//创建一个新节点,插入头部,此时newnode变成新的头节点SLTNode* newnode = SLTBuyNode(x);//创建一个新节点newnode->next = *pphead;//让该节点指向原来的头节点,连接两个节点 *pphead = newnode;//让newnode成为新的头,*pphead指向newnode,因为*pphead指向的都是头节点
}

       注:newnode->next= *pphead和*pphead = newnode不能调换顺序,否则原头节点的数据会丢失!

5.3 指定位置之前插入

三种情况:

1、pos在中间任意位置的情况:

2、pos在前面(尾节点) 的情况:

3、pos在尾节点的情况:

         一般情况下,要找到pos的前一个结点prev,让prev的next指向新结点,而新节点的next指向pos,因为该函数要实现在指定位置之前插入,所以pos传空则没有意义,所以pos不能为空,因为pos不能为空,所以链表也不可能为空,因为要找pos的前一个结点,如果pos恰好就是头结点,那么就相当于是头插了,直接调用之前封装的头插函数。

整体代码如下: 

//在指定位置pos之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//**pphead:代表第一个节点。pos可能是头节点,所以传入二级指针
{//1、为什么传入二级指针?assert(pphead && *pphead);//*pphead链表不能为空:如果链表为空,pos也为空,就不能在pos之前插入数据。有pos节点那就说明链表不是空链表assert(pos);SLTNode* newnode = SLTBuyNode(x);//若pos==*pphead,说明是头插if (pos == *pphead){SLTPushFront(pphead, x);//调用头插}else{SLTNode* prev = *pphead;//从第一个节点开始找pos前一个节点while (prev->next != pos){prev = prev->next;}//找到了这个节点posnewnode->next = pos;prev->next = newnode;}
}

5.4 指定位置之后插入

有两种情况:

1、

2、

这种错误方法的解决办法:先存储一下尾节点,然后……

代码如下:

//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{//为什么没有头节点参数?如果是最后一个节点,他是前一个节点prev->next=pos,用不到头节点参数assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}

六、删除

6.1 尾删

在一般情况下,我们需要找到尾节点的前一个结点prev,让他的next指针指向NULL,然后再释放尾结点并置空,因为我们需要找尾结点的前一个结点,如果该链表恰好只有一个结点时,是没有前结点的,所以单独讨论,该情况下直接将唯一的结点释放掉即可。当链表为空的时候,删除操作是没有意义的,所以要直接使用断言制止这种情况!

画图示范:

//尾删
void SLTPopBack(SLTNode** pphead)
{assert(*pphead && *pphead);//链表不能为空,*pphead就不能为空//找尾节点://尾节点释放掉后,需要把他前面的节点置为空。否则,该节点就会变成一个野指针//链表只有一个节点if ((*pphead)->next == NULL)//->的优先级高于*,所以要在*pphead上加括号{free(*pphead);*pphead = NULL;}else{//链表有多个节点SLTNode* prev = *pphead;//用来记录尾结点的前一个结点SLTNode* ptail = *pphead;//用来遍历链表,最后释放尾结点while (ptail->next){prev = ptail;ptail = ptail->next;}//此时prev恰好是尾结点的前一个结点//此时ptail恰好是尾结点,将其释放并置空free(ptail);ptail = NULL;prev->next = NULL;}	
}

1、 如果*pphead为空,说明链表为空,但是链表不能为空;pphead也不能为空

ptail指向的next要置为空,避免ptail成为空指针。

2、尾节点删除(释放)后,为什么还要将前一个节点置为空?

        前一个节点指向的空间就是最后一个节点,只有一个节点被释放掉后,空间还存在,但是已经没有使用权限了,而前一个节点prev的next存储的地址仍然是指向那个节点的,prev的next成为了一个野指针。

3、

6.2 头删

        一般情况下,令*pphead指向第二个结点,然后释放掉第一个结点即可,链表只有一个结点的情况,也是释放掉第一个结点,所以不需要分开讨论,链表为空时头删没有意义!!所以必须通过断言来制止!

画图分析:

 代码如下:

//头删
void SLTPopFront(SLTNode** pphead)
{//链表有多个节点:assert(pphead && *pphead);//直接释放掉头节点?:释放---第二个节点作为新的头节点,但是:怎么找到这个节点?//先存储下一个结点的next指针SLTNode* next = (*pphead)->next;//存储next值free(*pphead);//直接释放头节点*pphead = next;//判断这个方法在只有一个节点时能否适用
}

6.3 删除指定位置之前的节点

1. 链表有多个节点

2.链表只有一个节点

3.链表没有节点的情况:删除没有意义。

          一般情况下,要找到pos的前一个结点prev,让prev的next指向新结点,而新节点的next指向pos,因为该函数要实现在指定位置之前插入,所以pos传空则没有意义,所以pos不能为空,因为pos不能为空,所以链表也不可能为空,因为要找pos的前一个结点,如果pos恰好就是头结点,那么就相当于是头插了,直接调用之前封装的头插函数。

代码如下: 

//删除pos之前节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(*pphead && pphead);assert(pos);//删除pos节点://pos是头节点/不是头节点if (pos == *pphead){//头节点//SLTNode* next = (*pphead)->next;//free(*pphead);//*pphead = next;//也可以直接调用头删SLTPopFront(pphead);//传入二级指针}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}

6.4 删除指定位置之后的节点

两种情况:1. 

2. 

      一般情况下,令pos->next=pos->next->next因为是删除指定位置之后的结点,所以必须保证pos的后一个结点存在,要使用断言。 

完整代码如下:

//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;//pos  del  del->nextpos->next = del->next;free(del);del = NULL;
}

注:newnode->next = pos->next和pos->next = newnode不能调换顺序! 

如果交换了,

6.5 销毁链表

        链表和顺序表不一样,顺序表销毁可以直接销毁整个连续空间,而链表是由一个一个的独立的节点组成的,销毁链表就需要销毁一个个的节点。

代码如下:

//销毁链表void SLTDesTory(SLTNode** pphead)
{assert(pphead);//保证传入的参数不是NULLassert(*pphead);//保证链表不为空SLTNode* pcur = *pphead;//用来删除SLTNode* next = NULL;//用来遍历while (pcur){next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;//告诉编译器此时*pphead不能用了
//相当于毁了第一把钥匙,那后面的即使不置空也不会被使用到!
}

为什么最后只把*pphead置空,而不把他后面的其他结点置空?

      我们平时在动态内存释放的时候,其实空间已经返还给操作系统了,即使里面存在数据,也不影响别人的使用,因为直接覆盖就行了,所以我们之所以要置NULL,是为了防止我们写了很多代码后,忘记了其已经被释放,再去使用的话其实就是相当于使用了野指针,此时直接就会导致程序崩溃,所以置NULL,是为了让编译器提醒我们,这块空间不能被使用,在编译的时候,就可以即使地发现自己的问题。所以在这里,*pphead是找到后续链表其他结点的关键,只要*pphead被置空了,就相当于第一把钥匙丢了,那么后续的结点是不可能会被我们误用的

七、查找

       在指定位置之前插入、指定位置之后插入、删除指定位置结点、删除指定位置之后的结点,都涉及到指定位置,所以我们封装一个查找函数,根据我们需要查找的数据,返回该数据所在的结点。

代码如下:

//查找:链表的遍历
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//phead指向链表
{SLTNode* pcur = phead;//记录数据,可以不用改变链表的本来的值while (pcur)//pcur != NULL,链表不为空,进入循环{if (pcur->data == x){return pcur;}pcur = pcur->next;}//没有进入循环,说明链表为空return NULL;
}

八、单链表实现的所有代码

SList.h

#pragma once//相关可能用到的头文件
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>//定义节点的结构
//data+指向下一个节点的指针typedef int SLTDataType;//类型用SLDataType来表示typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;//重命名一个简单的名字void SLTPrint(SLTNode* phead);//从链表的头phead开始打印//链表的接口定义,
//尾插
void SLTPushBack(SLTNode**  pphead, SLTDataType x);//pphead头节点
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);//销毁链表
void SListDesTroy(SLTNode** pphead);

SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"//链表的打印:
void SLTPrint(SLTNode* phead)//链表的头节点
{SLTNode* pcur = phead;while (pcur)//等价于pcur != NULL{printf("%d->", pcur->data);pcur = pcur->next;//两个节点通过next打印输出5}printf("NULL\n");//此时pcur为NULL,不需要置空了
}//当插入节点时,都在重复判断空间是否充足,然后再malloc申请空间,
// 因此此功能单独提取出来
SLTNode* SLTBuyNode(SLTDataType x)//SLTNode*为返回值
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){//空间申请失败!perror("malloc fail!");exit(1);//异常退出(非0退出)}newnode->data = x;newnode->next = NULL;//return newnode;
}
//尾插
//要先抓到尾节点,再将尾节点和新节点链接起来
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);//*pphead可以为空//*pphead是指向第一个节点的指针SLTNode* newnode = SLTBuyNode(x);//建立一个新节点if (*pphead == NULL)//判断第一个节点是否为空{//空链表:申请一个新的节点,让phead指向这个新的节点,此时,链表就变成只有一个节点的链表*pphead = newnode;//}else{//非空链表:找尾节点,从头开始找SLTNode* ptail = *pphead;//定义ptail先指向头节点,然后从头往后(循环)开始找尾巴while (ptail->next)//->表示指针的解引用{ptail = ptail->next;//指向下一个节点,继续往下走}//此时ptail指向的就是尾节点//连接尾节点之前需要一个新节点,在进行新节点的创建ptail->next = newnode;}
}                                                                  //有诈:最开始是有一个空链表,phead指向空节点,ptail为空时,->:表示指针的解引用,就不可以对空指针进行解引用,//调试发现:行参变了,但是实参没变,说明是传值调用//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);//判断二级指针接收的内容是否为空//创建一个新节点,插入头部,此时newnode变成新的头节点SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;//让该节点指向原来的头节点,连接两个节点 *pphead = newnode;//让newnode成为新的头,*pphead指向newnode,因为*pphead指向的都是头节点}//尾删
void SLTPopBack(SLTNode** pphead)
{assert(*pphead && *pphead);//链表不能为空,*pphead就不能为空//画图//找尾节点://尾节点释放掉后,需要把他前面的节点置为空。否则,该节点就会变成一个野指针//链表只有一个节点if ((*pphead)->next == NULL)//->的优先级高于*,因为要先解引用,所以加一个括号{free(*pphead);*pphead = NULL;}else{//链表有多个节点SLTNode* prev = *pphead;SLTNode* ptail = *pphead;while (ptail->next){prev = ptail;ptail = ptail->next;}free(ptail);ptail = NULL;prev->next = NULL;}
}//头删
void SLTPopFront(SLTNode** pphead)
{//链表有多个节点:assert(pphead && *pphead);//直接释放掉头节点?:释放---第二个节点作为新的头节点,但是:怎么找到这个节点?//先存储下一个结点的next指针SLTNode* next = (*pphead)->next;//存储next值free(*pphead);//直接释放头节点*pphead = next;//判断这个方法在只有一个节点时能否适用
}//查找:链表的遍历
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//phead指向链表
{SLTNode* pcur = phead;//记录数据,可以不用改变链表的本来的值while (pcur)//pcur != NULL,链表不为空,进入循环{if (pcur->data == x){return pcur;}pcur = pcur->next;}//没有进入循环,说明链表为空return NULL;
}//在指定位置pos之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//**pphead:代表第一个节点。pos可能是头节点,所以传入二级指针
{//1、为什么传入二级指针?assert(pphead && *pphead);//*pphead链表不能为空:如果链表为空,pos也为空,就不能在pos之前插入数据。有pos节点那就说明链表不是空链表assert(pos);SLTNode* newnode = SLTBuyNode(x);//若pos==*pphead,说明是头插if (pos == *pphead){SLTPushFront(pphead, x);//调用头插}else{SLTNode* prev = *pphead;//从第一个节点开始找pos前一个节点while (prev->next != pos){prev = prev->next;}//找到了这个节点posnewnode->next = pos;prev->next = newnode;}
}//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{//为什么没有头节点参数?如果是最后一个节点,他是前一个节点prev->next=pos,用不到头节点参数assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}//删除pos之前节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(*pphead && pphead);assert(pos);//删除pos节点://pos是头节点if (pos == *pphead){//头节点//SLTNode* next = (*pphead)->next;//free(*pphead);//*pphead = next;//也可以直接调用头删SLTPopFront(pphead);//传入二级指针}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;//pos  del  del->nextpos->next = del->next;free(del);del = NULL;
}//销毁链表
void SListDesTroy(SLTNode** pphead)
{//销毁一个一个的节点assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}

写到这儿不容易,看到这儿也不容易,所以,要不点个小心心,三连支持一下啊?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/4800.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Selenium IDE 常见错误笔记

错误1&#xff1a;Failed:Exceeded waiting time for new window to appear 2000ms 这个错误通常出现在第一次运行时&#xff0c;有两个原因&#xff1a; Firefox阻止了弹出式窗口&#xff0c;在浏览器设置里允许这个操作即可。 有些网站设置了反扒机制&#xff0c;脚本运行…

解决Blender导出FBX文件到Unity坐标轴错误的问题

发现Blender的模型导入到Unity里面有问题,简单研究了下发现是坐标系不同,Unity使用的是左手坐标系,Blender使用的是右手坐标系 。 下面直接将如何解决 首先忽略Blender的右手坐标系以及Z轴朝上的事&#xff0c;依照unity坐标系情况修改模型物体的旋转&#xff0c;以Blender猴…

算法工程师——算法岗的分类及要求汇总

算法岗工程师 根据 Talent Seer 人才报告显示,全球 AI 从业者总人数约有 30 万,还是供不应求,其中 AI 技术专家(具有相关领域博士学位及 3 年以上工作经验的)约有 3.65 万。 简介 对于计算机专业的毕业生而言,算法岗基本上就是 「高薪」 的代名词。 在当今 IT 行业,算…

【城市】2023深圳市定居与生活相关政策(含租房、租车)

【城市】2023深圳市定居与生活相关政策&#xff08;含租房、租车&#xff09; 文章目录 一、户籍身份1、深圳市居住登记凭证、居住证&#xff08;点击就送&#xff09;2、深圳落户&#xff08;点击就送1&#xff09; 二、人才补贴人才引进补贴&#xff08;含应届生&#xff09;…

vue3左树的全选和反选

<el-input v-model"filterText" placeholder"" style"width: 48%"/><el-button type"primary" click"handleSearch" class"ml-2">查找</el-button><el-radio-group v-model"form.choic…

学习笔记:能量信号与功率信号(一)

目录 一、能量信号&#xff08;Energy Signal&#xff09; 二、功率信号&#xff08;Power Signal&#xff09; 三、信号关系图 四、总结 能量信号和功率信号是信号分析中两个基本的概念&#xff0c;它们主要用来描述信号在时间域中能量分布的特性&#xff0c;对于理解信号…

Faststone Capture:一触即发的效率革命【AI写作】

首先&#xff0c;这篇文章是基于笔尖AI写作进行文章创作的&#xff0c;喜欢的宝子&#xff0c;也可以去体验下&#xff0c;解放双手&#xff0c;上班直接摸鱼~ 按照惯例&#xff0c;先介绍下这款笔尖AI写作&#xff0c;宝子也可以直接下滑跳过看正文~ 笔尖Ai写作&#xff1a;…

苹果发布开源模型;盘古大模型5.0将亮相;英伟达将收购 Run:ai

苹果首次发布开源语言模型 近期&#xff0c;苹果在 Hugging Face 发布了 OpenELM 系列模型。OpenELM 的关键创新是逐层扩展策略&#xff0c;该策略可在 transformer 模型的每一层中有效地分配参数&#xff0c;从而提高准确性。 与具有统一参数分配的传统语言模型不同&#xff…

【免费Java系列】给大家出一些JavaSE基础第八天的内容案例 , 让大家更好的理解与掌握

String字符串 案例一 求取字符串的长度 public class Main {public static void main(String[] args) {String str "Hello World";String substring str.substring(6);System.out.println("截取后的字符串为&#xff1a;" substring);} }输出结果&…

windows下pysqlite3安装

pysqlite3 下载地址&#xff1a;SQLite Download Page windows下安装 首先在官网中下载以下文件 sqlite-amalgamation-3450300.zip #源码文件 sqlite-dll-win-x64-3450300.zip # 根据系统选择32或者64&#xff0c;可通过查看我的电脑属性中查看 sqlite-tools-win-x64-345…

springboot笔记一:idea社区版本创建springboot项目的方式

社区idea 手动maven 创建springboot项目 创建之后修改pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:sc…

STM32使用PWM控制舵机

系列文章目录 STM32单片机系列专栏 C语言术语和结构总结专栏 文章目录 1. 舵机简介 2. 硬件连接 3. 代码实现 3.1 PWM.c 3.2 PWM.h 3.3 Servo.c 3.4 Servo.h 3.5 main.c 3.6 完整工程文件 PWM和OC输出详解&#xff1a; STM32定时器的OC比较和PWM​​​​​​​ 1. …

CentOS 7虚拟机配置过程中所需组件的安装(二)

1.安装net-tools组件&#xff08;解决无 ifconfig&#xff09; # yum install net-tools 2.安装gcc、c编译器以及内核文件 # yum -y install gcc gcc-c kernel-devel 验证安装成功 3.安装nano&#xff08;文本编辑器&#xff09; # yum install nano

python监听html click教程

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 Python实现监听HTML点击事件 在Web开发中&#xff0c;经常需要在用户与页面交互时执行一些…

500V 直流充电桩测试负载箱的基础知识

500V直流充电桩测试负载箱是一种专门用于测试和验证直流充电桩性能的设备。它能够模拟真实的充电过程&#xff0c;对充电桩的输出电压、电流、功率等参数进行精确的测量和控制&#xff0c;以确保充电桩的稳定性和安全性。 一、工作原理 500V直流充电桩测试负载箱主要由电源模块…

【Linux】Linux上如何挂载磁盘

目录 一、查看磁盘使用情况df -h 查看已挂载磁盘的大概使用情况fdisk -l 查看主机上所有磁盘设备的详细情况lsblk 查看主机上所有磁盘设备基本信息 二、进行磁盘挂载mkfs 在特定的分区上建立 linux 文件系统mount 进行磁盘挂载文件系统配置 一、查看磁盘使用情况 df -h 查看已…

3分钟了解拍摄VR全景需要哪些硬件

VR全景图片是一张水平方向360度&#xff0c;垂直方向180度&#xff0c; 图片尺寸宽高比为2:1的图片。 通过720yun APP或720yun官网上传生成全景H5页面&#xff0c;即可360度全方位观看画面中的景象。 拍摄VR全景有很多方法&#xff0c;下面介绍用单反相机、全景相机、智能手机…

OceanBase开发者大会实录-杨传辉:携手开发者打造一体化数据库

本文来自2024 OceanBase开发者大会&#xff0c;OceanBase CTO 杨传辉的演讲实录—《携手开发者打造一体化数据库》。完整视频回看&#xff0c;请点击这里&#xff1e;> 各位 OceanBase 的开发者&#xff0c;大家上午好&#xff01;今天非常高兴能够在上海与大家再次相聚&…

使用这 7 个绩效评估模板简化您的员工评估

绩效评估受到了不好的评价&#xff1b;员工发现它们压力很大&#xff0c;而管理者则发现它们很耗时。 但随着绩效管理成为 2024 年人力资源的首要任务&#xff0c;也许是时候重新思考了。绩效评估模板可以帮助减轻评估过程的麻烦。通过为管理者提供一种简单、标准化的方法来评…

今日早报 每日精选15条新闻简报 每天一分钟 知晓天下事 4月29日,星期一

每天一分钟&#xff0c;知晓天下事&#xff01; 2024年4月29日 星期一 农历三月廿一 1、 移民管理局&#xff1a;5月6日起换发补发出入境证件可“全程网办”。 2、 文旅部宣布恢复两岸旅游新措施&#xff1a;将率先恢复福建居民到马祖旅游。 3、 网信办&#xff1a;整治用夸…