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;

		}//找到了这个节点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;
}

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

  • 97
    点赞
  • 83
    收藏
    觉得还不错? 一键收藏
  • 90
    评论
评论 90
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值