DS:单链表的实现

news2024/11/26 22:22:48

欢迎各位来到 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;

		}//找到了这个节点pos
		newnode->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->next
	pos->next = del->next;
	free(del);
	del = NULL;
}

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

如果交换了,

6.5 销毁链表

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

代码如下:

//销毁链表

void SLTDesTory(SLTNode** pphead)
{
	assert(pphead);//保证传入的参数不是NULL
	assert(*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;

		}//找到了这个节点pos
		newnode->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->next
	pos->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.coloradmin.cn/o/1625753.html

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

相关文章

20232937文兆宇 2023-2024-2 《网络攻防实践》实践七报告

20232937文兆宇 2023-2024-2 《网络攻防实践》实践七报告 1.实践内容 &#xff08;1&#xff09;使用Metasploit进行Linux远程渗透攻击 任务&#xff1a;使用Metasploit渗透测试软件&#xff0c;攻击Linux靶机上的Samba服务Usermap_script安全漏洞&#xff0c;获取目标Linux…

深入Linux下的GCC编译器:从入门到精通

目录标题 1、GCC编译器概述2、安装GCC3、GCC的基本使用4、高级功能4.1 多文件编译4.2 静态和动态链接4.3 什么是链接&#xff1f;4.4 静态链接优点缺点 4.5 动态链接优点缺点 4.6 实际应用4.7 编译优化 GCC&#xff08;GNU Compiler Collection&#xff09;是一款免费、开源的编…

累积流量计算(MODBUS RTU通信数据处理)

1、常用通信数据处理 MODBUS通信系列之数据处理_modbus模拟的数据变化后会在原来的基础上累加是为什么-CSDN博客文章浏览阅读1k次,点赞2次,收藏2次。MODBUS通信专栏有详细文章讲解,这里不再赘述,大家可以自行查看。链接如下:SMART S7-200PLC MODBUS通信_RXXW_Dor的博客-C…

操作系统(Operating System)知识点复习——第十一章 I/O管理与磁盘调度

目录 0.前言 1.I/O设备 2.I/O功能的组织 3.Operating System Design Issues 4.I/O缓冲 4.1 单缓冲Single Buffer 4.2 双缓冲Double Buffer 4.3 循环缓冲 5.磁盘调度Disk Scheduling 5.1 磁盘性能参数 5.2 磁盘调度策略 ①First-in&#xff0c;first-out(FIFO) ②Pr…

芯片胶点胶加工的效果和质量的检测方法有哪些?

芯片胶点胶加工的效果和质量的检测方法有哪些&#xff1f; 芯片胶在电子封装领域用的是比较多的&#xff0c;特别是高度精密集成芯片器件。那么如何判断点胶后的效果和质量的好与坏&#xff1f; 芯片胶点胶加工的效果和质量的检测是一个重要的环节&#xff0c;以确保产品满足设…

医院能耗监测管理系统,助力医院节能减排

医院属于大型建筑&#xff0c;由于医院能耗计量点位繁多&#xff0c;数据采集大多采用传统的人工模式&#xff0c;很难保证计量管理的准确性和科学性。为了对医院能耗进行精细化管理&#xff0c;需要建立能耗管理系统&#xff0c;在辅助成本核算工作的同时&#xff0c;可以实时…

Java学习笔记29(泛型)

1.泛型 ArrayList<Dog> arrayList new ArrayList<Dog>(); //1.当我们ArrayList<Dog>表示存放到ArrayList集合中的元素是Dog类 //2.如果编译器发现添加的类型&#xff0c;不满足要求&#xff0c;就会报错 //3.在便利的时候&#xff0c;可以直接取出Dog类型而…

8个拿来即用的Python自动化脚本!

每天你都可能会执行许多重复的任务&#xff0c;例如阅读新闻、发邮件、查看天气、清理文件夹等等&#xff0c;使用自动化脚本&#xff0c;就无需手动一次又一次地完成这些任务&#xff0c;非常方便。而在某种程度上&#xff0c;Python 就是自动化的代名词。 今天分享 8 个非常…

BGP的基本配置

l 按照以下步骤配置BGP协议&#xff1a; 第1步&#xff1a;设备基本参数配置&#xff0c;AS内配置IGP确保内部网络连通性&#xff1b; l 配置IGP&#xff08;OSPF协议等&#xff09;路由解决peer对等体的源和目标IP之间连通性&#xff0c;确保peer之间TCP&#xff08;179&a…

如何查看自己的公网IP?

我们在网络中&#xff0c;每一个设备都被分配了一个唯一的IP地址&#xff0c;用以区分和识别其他设备。公网IP地址是指可被公众访问的IP&#xff0c;是因特网上的全球唯一标识。当我们需要查看自己的公网IP时&#xff0c;可以采取以下几种方式。 使用命令行查看公网IP 在Windo…

SpringCloud 之 服务提供者

前提 便于理解,我修改了本地域名》这里!!! 127.0.0.1 eureka7001.com 127.0.0.1 eureka7002.com 127.0.0.1 eureka7003.com学习Rest实例之提供者 提供者模块展示 1、导入依赖 <!-- 实体类 Web--><dependency><groupId>com.jyl</groupId><…

光电离子传感器PID-AH5在空气质量监测和HVAC系统中的应用

随着工业化和城市化的步伐不断加快&#xff0c;空气质量问题日益严重&#xff0c;对人们的健康和生活品质构成了严重威胁。为了有效监测和改善空气质量&#xff0c;光电离子传感器作为一种先进的检测技术&#xff0c;正在空气质量监测以及HVAC&#xff08;供暖、通风和空调&…

OpenHarmony实战开发-状态变量组件定位工具实践

概述 自定义组件中的变量被状态装饰器&#xff08;State&#xff0c;Prop等&#xff09;装饰后成为状态变量&#xff0c;而状态变量的改变会引起使用该变量的UI组件渲染刷新。状态变量的不合理使用可能会带来冗余刷新等性能问题。开发者可以使用状态变量组件定位工具获取状态管…

JdbcTemplate详解

1 概述 为了使JDBC更加易于使用&#xff0c;Spring在JDBC API上定义了一个抽象层&#xff0c;以此建立一个JDBC存取框架。 作为Spring JDBC框架的核心&#xff0c;JDBC模板的设计目的是为不同类型的JDBC操作提供模板方法&#xff0c;通过这种方式&#xff0c;可以在尽可能保留…

【数据结构(邓俊辉)学习笔记】向量02——动态空间管理

文章目录 1. 概述2. 静态空间管理缺点3. 动态空间管理3.1 扩容3.1.1 如何实现扩容3.1.2 扩容算法3.1.3 容量递增策略 VS 容量倍增策略3.1.3.1 容量倍增策略分摊分析3.1.3.2 容量递增策略分摊分析3.1.3.3 结果对比 3.2缩容3.2.1 动态缩容算法实现3.2.2 动态缩容算法时间复杂度 4…

Sui主网升级至V1.23.1版本

其他升级要点如下所示&#xff1a; #17126 协议&#xff1a;Deepbook的更改将被还原。 #16673 开发者可能会看到更多编译器诊断&#xff0c;因为选择的解析错误不再阻止编译&#xff0c;并且编译器的诊断会到达后续编译阶段&#xff0c;其中可能会生成额外的诊断。 #16966…

SQLite FTS5 扩展(三十)

返回&#xff1a;SQLite—系列文章目录 上一篇:SQLite的知名用户(二十九) 下一篇:SQLite 的命令行 Shell(三十一&#xff09; 1. FTS5概述 FTS5 是一个 SQLite 虚拟表模块&#xff0c;它为数据库应用程序提供全文搜索功能。在最基本的形式中&#xff0c; 全文搜索引擎允许…

【LAMMPS学习】八、基础知识(4.5)TIP5P水模型

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语&#xff0c;以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各…

JVS物联网平台驱动网关管理:智能化与自动化的探索

驱动网关的管理 驱动网关是对驱动进程进行生命周期的管理&#xff0c;包括驱动进程的创建、启停、更新、结束等管理功能。 如下图所示&#xff1a; 驱动代理程序是驱动网关的管理程序&#xff0c;每个驱动网关对应于一个驱动代理程序&#xff0c;驱动代理程序初始版本是安装在…

Nginx解决跨域访问难题:轻松实现跨域资源共享!

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ 跨域资源共享&#xff08;CORS&#xff0c;Cross-Origin Resource Sharing&#xff09;是一种网络浏览器的安全功能&#xff0c;它限制了一个源&#xff08;域、协议和端口…