数据结构之单链表非常详细介绍(适合小白)

之前有一篇文章介绍完顺序表,可以点击(顺序表文章)即可看到顺序表的知识后,我们就要开始学习链表了,链表的种类有很多,比如说单链表双向链表循环或者非循环链表以及带头或者不带头链表等,那么链表和顺序表有哪些不同呢,相较于顺序表,链表做了哪些改变呢,有什么优势呢?今天我就带大家先了解最简单的链表——单链表。

一万字详细解说单链表,顺序表有关知识详细解答~❤️


目录

一.链表的概念及结构

1.1链表的概念

1.2 链表的结构

二.单链表和顺序表区别

2.1名字区别 

2.1单链表和顺序表的区别与优缺点

​编辑

三.八种链表类型

ps:头指针和头结点

ps:带哨兵位和不带哨兵位

3.1单向带头循环链表

3.2单向带头非循环链表

3.3单向不带头循环链表

3.4 单向不带头非循环链表

3.5 双向带头循环链表

3.6双向带头非循环链表

3.7 双向不带头循环链表

3.8双向不带头非循环链表

 四.单链表的说明与实现

单链表的定义

单链表的特点

单链表的实现

4.1单链表的结构定义

4.2 链表的功能

4.3链表的功能实现

ps:assert断言的使用

4.3.1打印链表

4.3.2创建结点

4.3.3 单链表尾插

4.3.4 单链表尾删

4.3.5单链表头删

4.3.6单链表的头插

4.3.7查找某个结点

4.3.8删除某个结点

4.3.9单链表删除pos位置之后的结点

4.3.10单链表结点修改

​编辑

4.3.11 单链表pos前插入结点 

4.3.12单链表pos后插入结点 

4.3.13对 4.3.7~~4.3.12实践检测

4.3.14销毁链表

五.源代码 

1.SL.h

2.SL.c 

3.test-SL.c


一.链表的概念及结构

1.1链表的概念

概念:链表是一种物理存储结构连续,非顺序的存储结构,但链表在逻辑上连续的,顺序的,而数据元素的逻辑顺序是通过链表中的指针连接次序实现的。

1.2 链表的结构

链表是由一个个结点组成的,结点如下图所示:

数据结构之单链表非常详细介绍(适合小白)_第1张图片

 链表中的最后一个结点的next指向空,next=NULL

一个个结点串成了链表,如下图所示:

数据结构之单链表非常详细介绍(适合小白)_第2张图片 

  有人可能会有疑问,不是说链表只是在逻辑结构上是连续的,在物理存储结构上是不连续的,那为什么上图中一个个结点明明是挨在一起的,那么它在物理存储结构上肯定是连续的呀,其实不然,上图是为了方便大家理解,才用线条连接了结点,实际上在内存中,每个结点可能会隔得很远,仔细观察每个结点上面的红色文字,那就是这个结点的地址,而蓝色文字是下一个结点的地址,很明显能看到这两个结点并不是相邻的,因此也验证了顺序表在逻辑结构上确实是连续的,但在物理存储结构上确实是不连续的。


二.单链表和顺序表区别

2.1名字区别 

单链表的英文名——single linked list,我们简写成SL

顺序表的SeqList或者SQL

2.1单链表和顺序表的区别与优缺点

这是两种不同的存储结构,我们先谈谈区别吧,

顺序表是顺序存储结构它的特点是逻辑关系上相邻的两个元素在物理位置上也相邻

但是链表不同

链表是链式存储结构,特点是不需要逻辑上相邻的元素在物理位置上也相邻

因为链式存储结构可以通过结点中的指针域直接找到下一个结点的位置。

数据结构之单链表非常详细介绍(适合小白)_第3张图片

 **单链表的优缺点:
1.优点:可以按照实际所需创建结点增减链表的长度,更大程度地使用内存。
2.缺点:进行尾部或者任意位置上插入或删除时时间复杂度和空间复杂度较大,每次都需要通过指针的移动找到所需要的位置,相对于顺序表查找而言效率较低。

互补关系

**顺序表的优缺点:
1.优点:可以通过下标直接访问所需要的数据
2.缺点:不能按实际所需分配内存,只能使用malloc或者realloc函数进行扩容,容易实现频繁扩容,容易导致内存浪费与数据泄露等问题

ps:我们拿一个指针指向一块连续的空间,然后有一个size记录当前有效数据,capicity记录容量,这样好处是我们可以通过下标来访问其中每一个数据,但缺点是当我们开辟数组的时候,第一个数组容量可能是10,然后我们放11个元素进去,这个时候空间不够,我们就重新开辟空间,一般我们开辟空间都是直接将原空间翻倍,但这里20个空间只放11个元素,空间就有些浪费了。


三.八种链表类型

在了解链表的类型之前,我们需要了解链表的几个特点

1.单向和双向
2.带哨兵位和不带哨兵位
3.循环和非循环

我们可以通过组合的方式,如:单向带头循环链表,双向不带头非循环链表

数据结构之单链表非常详细介绍(适合小白)_第4张图片

 每一层都有2种类型,所以2*2 *2总共有8种类型链表

ps:头指针和头结点

通常会用头指针来标识一个单链表,头指针为NULL时表示一个空表。但是,为了操作方便,会在单链表的第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点。如下图所示:

头结点数据域可以不设置值,指针域指向第一个元素的结点

数据结构之单链表非常详细介绍(适合小白)_第5张图片

 头结点和头指针的区分:不管带不带头结点,头指针始终指向单链表的第一个结点,而头结点是带头结点的单链表中的第一个结点,结点内通常不存储信息。 

ps:带哨兵位和不带哨兵位

带头结点之后,什么情况下形参必须传二级指针(或者一级指针的引用)

***如果链表有头结点,那么头指针就是指向头结点数据域的指针。 

数据结构之单链表非常详细介绍(适合小白)_第6张图片

数据结构之单链表非常详细介绍(适合小白)_第7张图片 

***单链表也可以没有头结点,没有头结点的单链表  

数据结构之单链表非常详细介绍(适合小白)_第8张图片

数据结构之单链表非常详细介绍(适合小白)_第9张图片  


 3.1单向带头循环链表

数据结构之单链表非常详细介绍(适合小白)_第10张图片数据结构之单链表非常详细介绍(适合小白)_第11张图片

 3.2单向带头非循环链表

数据结构之单链表非常详细介绍(适合小白)_第12张图片数据结构之单链表非常详细介绍(适合小白)_第13张图片

 3.3单向不带头循环链表

数据结构之单链表非常详细介绍(适合小白)_第14张图片数据结构之单链表非常详细介绍(适合小白)_第15张图片

3.4 单向不带头非循环链表

数据结构之单链表非常详细介绍(适合小白)_第16张图片数据结构之单链表非常详细介绍(适合小白)_第17张图片

3.5 双向带头循环链表

数据结构之单链表非常详细介绍(适合小白)_第18张图片数据结构之单链表非常详细介绍(适合小白)_第19张图片

 3.6双向带头非循环链表

数据结构之单链表非常详细介绍(适合小白)_第20张图片数据结构之单链表非常详细介绍(适合小白)_第21张图片

3.7 双向不带头循环链表

数据结构之单链表非常详细介绍(适合小白)_第22张图片数据结构之单链表非常详细介绍(适合小白)_第23张图片

 3.8双向不带头非循环链表

数据结构之单链表非常详细介绍(适合小白)_第24张图片数据结构之单链表非常详细介绍(适合小白)_第25张图片

 四.单链表的说明与实现

单链表的定义

由于顺序表的插入删除操作需要移动大量的元素,影响了运行效率,因此引入了线性表的链式存储——单链表。单链表通过一组任意的存储单元来存储线性表中的数据元素,不需要使用地址连续的存储单元,因此它不要求在逻辑上相邻的两个元素在物理位置上也相邻

单链表的特点

  1. 单链表不要求逻辑上相邻的两个元素在物理位置上也相邻,因此不需要连续的存储空间。
  2. 单链表是非随机的存储结构,即不能直接找到表中某个特定的结点。查找某个特定的结点时,需要从表头开始遍历,依次查找。

包括俩个域:

1.数据域: 存储数据元素信息的域

2.指针域:存储直接后继存储位置的域

指针域中存储的信息称为指针或链,n个结点(数据元素的存储映像)链接成一个链表

每个结点中只包含一个指针域,称为线性链表或单链表

 对于每个链表结点,除了存放元素自身的信息外,还需要存放一个指向其后继的指针。 

typedef int SLDateType;
typedef struct SLTNode定义单链表结点类型
{
	SLDateType data;//数据域,可以是别的各种数据类型,只用将上面的int改成其他类型即可
	struct SLTNode* next;指针域
}SLTNode;

单链表的实现

下面我们实现的单链表是很多数据结构的子结构,也就是单向不带头非循环链表

4.1单链表的结构定义

单链表的结构与顺序表是完全不同的,分为俩个部分数据域和指针域

typedef int SLDateType;
typedef struct SLTNode
{
	SLDateType data;
	struct SLTNode* next;
}SLTNode;

4.2 链表的功能

链表要实现那些功能呢?其实这些功能我们都很熟悉,数据结构无非是对数据进行管理,要实现数据的增删查改,因此链表的基本功能也都是围绕着数据的增删查改展开。

//创建一个结点
SLTNode* BuyListNode(SLTDateType x);
//销毁单链表
void SLTDestory(SLTNode** pphead);
//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDateType x);
//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x);
//单链表头删
void SLTPopFront(SLTNode** pphead);
//单链表尾删
void SLTPopBack(SLTNode** pphead);
//单链表结点查找
SLTNode* SLTNodeFind(SLTNode* phead, SLTDateType x);
//单链表结点删除(删除pos位置的结点)
void SLTErase(SLTNode** pphead, SLTNode* pos);
//单链表结点删除(删除pos位置之后的结点)
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos);
//单链表结点插入(在pos之前插入)
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
// 单链表结点插入(在pos之后插入)
void SLTInsertBack(SLTNode** pphead, SLTNode* pos, SLTDateType x);
// 单链表结点修改
void SLTModify(SLTNode* phead, SLTNode* pos, SLTDateType x);
//打印单链表
void SLTPrint(SLTNode* phead);

如何分别传送一级指针与二级指针
我们可以看到上面的接口,有的使用一级指针,而有的则使用二级指针。

这是为什么呢?->这是因为有些接口需要涉及改变实参(更改节点内部的内容),所以

需要二级指针传参,如果使用一级指针改变实参是无法修改实参的,使用一级指针只会修改这个一级指针的指针变量的地址,无法找到实参的地址。

比如:如果要改变链表的头指针就传二级指针,改变头指针不能传一级指针因为传送的过程就是拷贝的过程,相当于将头指针复制了一份,形参的改变不会影响实参,因此要改变链表的头指针需要传送二级指针。

若传入二级指针时,必须assert断言其是否为空。而若为一级指针,则无需判断,因为若链表为空,则其值便是NULL。

4.3链表的功能实现

ps:assert断言的使用

assert这个函数在指针传参的时候非常好用,在链表中我们用来判断链表指针是否为空。

而且assert函数在调试的时候非常好用,一旦指针为空,会立刻报错,**而且会帮我提示报错在哪个文件和在哪行代码。**在以后实战项目中对我们有很好的帮助,所以要善于利用assert函数

断言

空链表可以打印,不用断言

空链表能尾插,*pphead不用断言,pphead需要断言,因为pphead是指向plist的地址,它永远不能为空

空链表能头插,*pphead不用断言,pphead需要断言,因为pphead是指向plist的地址,它永远不能为空

空链表不能尾删头删,*pphead需要断言,pphead也需要断言,因为pphead是指向plist的地址,是头指针的地址

pphead需要在所有地方断言,因为永远不能为空

*pphead要结合场景来判断是否需要断言,比如插入不需要断言,空链表是可以为空的,但是删除是需要断言的,如果空链表删除是不可以的。


pphead是指向plist的地址,但是pphead解引用的值是plist的值,pphead本身就有地址,所以pphead是不可能是NULL的,pphead是指向plist头指针的地址。plist是头指针,plist是存放头结点的地址,plist解引用后就是头结点的值。

数据结构之单链表非常详细介绍(适合小白)_第26张图片


4.3.1打印链表

注意:链表和顺序不同的是,顺序表传过来的指针是肯定不会为空的,而链表传过来的指针是可能为空的,比如说当链表中没有元素时,头指针所指向的就是NULL,如果在第一行写上断言就会有问题。

当cur指向空的时候就可以停止打印了。

//打印单链表
void SLTPrint(SLTNode* phead)
{
    //不需要断言assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
    printf("NULL");
	printf("\n");
}

数据结构之单链表非常详细介绍(适合小白)_第27张图片 

在打完以上代码之后,我相信一定有很多疑惑,比如:

为什么不用断言?为什么将phead赋值给cur?为什么不是cur++而是cur->next?这三个疑惑也曾经疑惑着我,现在我明白了,我来给你们细细说上.....

为什么链表打印不用断言?而顺序表中指针都需要断言?

单链表:单链表中没有元素时,头指针所指向的就是NULL,结点表示空,结点为空直接打印空就行了,如果这个头指针指向的是NULL,那么就是个空表,如果在第一行写上断言就会有问题。空结点是代表没有结点也一样可以打印的,只是NULL而已。

顺序表:而顺序表是一个结构体包含了三个成员变量(a数组,size,capacity),指针指向一块数组空间,由size来决定是不是空,但是指向的空间都是空的,就不用size判断了。

为什么将phead赋值给cur?

当我们遍历单链表的时候,最好给一个另外的变量赋值去遍历,因为我们有时候会需要找到头的地址,为了不丢失头指针的地址,所以我们不用头指针phead自己遍历单链表。

cur=cur->next,为什么不是cur++?

结合逻辑和物理结构

在逻辑结构上是连续的,但是在物理结构上可能是不连续的,因此不能保证链表中每个结点都是连续的,所以不能cur++


通过对以上的分析,大家对细节部分更加理解通透,进行后面的功能实现吧


4.3.2创建结点

后面我们要在单链表中进行头插和尾插,此时插入的不再是像顺序表一样简单的SLDateType数据了,而是一个结点,这个结点是包括SLTDateType数据以及SLTDateType*的指针,因此,为了方便和减少代码的重复度,我们另写一个函数用来专门创建新结点

//创建一个结点
SLTNode* BuyListNode(SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail:");
		return 1;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

4.3.3 单链表尾插

注意:在创建结点时,已经让 结点.next=NULL,所以不需要在插入完结点后,再让新结点的next指针为NULL。

//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x)
{
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);
	//链表为空
	//链表不为空
	if (*pphead == NULL)
	{
		*pphead = newnode;
	//不需要让newnode->next=NULL,在BuySLTNode中我们就已经进行过这个操作了
	}
	else
	{
		//先找到链表的尾部
		SLTNode*tail= *pphead;
		while (tail->next!=NULL)
		{
			tail= tail->next;
		}
		tail->next =newnode;
	}
}

数据结构之单链表非常详细介绍(适合小白)_第28张图片

  

在敲完以上代码之后,大家肯定又有一些疑惑?

比如:为什么要传二级指针?为什么尾插又要断言了?为什么不是tail!=NULL而是tail->next!=NULL这俩个问题曾经也想过,现在给你们解答

为什么要传二级指针?

在函数中如果需要改变链表中的头节点地址,则传参的时候需要传址调用,结合参数已经是指针了,形参用二级指针接收。

因为结构体里面的next本身就是一个一级指针,尾插和头插,都会改变结构体里面存储的数据,而修改一级指针的内容就需要去二级指针来存储一级指针的地址,并传址才能改变单链表的内容,如果使用一级指针来存储一级指针的地址出了循环以后就会销毁,并不会影响单链表的内容也不能增删查改和管理存储数据。相当于形参与实参的差别。形参不会影响实参的改变,只是实参的临时的一份拷贝。所以要用二级指针。

链表为空的尾插:phead原先是空,需要尾插,需要将弄成二级指针,需要改变phead的值,所以要存下这个的地址,一级指针存一级指针的地址出了循环就销毁了,需要用二级指针来存储一级指针的地址,出了循环也会记住这个地址,记住phead新的地址,然后不为空了。

数据结构之单链表非常详细介绍(适合小白)_第29张图片

为什么断言?

至于这个什么时候要断言指针,什么时候不用断言指针:一级指针也就是phead,当链表为空的时候,phead就是为NULL,而二级指针永远指向phead,phead的地址是永远存在的,那么pphead就一定不可能为空,所以需要断言pphead。

为什么这里是cur->next!= NULL,而不是cur!=NULL

数据结构之单链表非常详细介绍(适合小白)_第30张图片


这些是我曾经出错的地方,还有什么需要解答的,可以在评论区写出疑惑,我会解答。


4.3.4 单链表尾删

要想删除链表中的元素,就必须保证原链表就有元素,因此要断言assert(*pphead)

//单链表尾删
void SLTPopBack(SLTNode** pphead)
{
	//0.链表为空
	assert(*pphead);
	assert(pphead);
	//1.链表一个结点
	if ((*pphead)->next== NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//2.多个结点
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;
		prev->next = tail;
	}
}

数据结构之单链表非常详细介绍(适合小白)_第31张图片

	//2.多个结点
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next!= NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next= NULL;
	}

数据结构之单链表非常详细介绍(适合小白)_第32张图片

 


  4.3.5单链表头删

头删需要注意的就是要断言即可

//单链表头删
void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead);
    assert(pphead);
	SLTNode*first= *pphead;
	*pphead = first->next;
	free(first);
	first= NULL;
}

数据结构之单链表非常详细介绍(适合小白)_第33张图片

 


 4.3.6单链表的头插

//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDateType x)
{
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

数据结构之单链表非常详细介绍(适合小白)_第34张图片

 


4.3.7查找某个结点

这个函数返回值不再是void,而是SListNode*,把找到的结点的地址返回去,这个函数一般会跟结点修改之类的函数一起使用。

//单链表结点查找
SLTNode* SLTNodeFind(SLTNode* phead, SLTDateType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

4.3.8删除某个结点

//单链表结点删除(删除pos位置的结点)
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(*pphead);
	assert(pphead);
    assert(pos);
	//头删
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	//非头删
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next!= pos)
		{
			prev = prev->next;
			assert(prev->next);
			//这里为什么需要断言
		}
		prev->next = pos->next;
		free(pos);
	}
}

注意:

  1. pos也要断言,pos可不能为空呀!
  2. prev->next也要断言,因为当prev->next为NULL时,说明整个链表的结点都排查完了,最后还是没有找到地址为pos的结点,证明pos传值有误。

数据结构之单链表非常详细介绍(适合小白)_第35张图片  

数据结构之单链表非常详细介绍(适合小白)_第36张图片


 4.3.9单链表删除pos位置之后的结点

//单链表结点删除(删除pos位置之后的结点)
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	assert(pphead);
	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

数据结构之单链表非常详细介绍(适合小白)_第37张图片

数据结构之单链表非常详细介绍(适合小白)_第38张图片


4.3.10单链表结点修改

// 单链表结点修改
void SLTModify(SLTNode* phead, SLTNode* pos, SLTDateType x)
{
	assert(pos);
	SLTNode* cur = phead;
	while (cur!=pos)
	{
		cur = cur->next;
		assert(cur);
	}
	pos->data = x;
}

数据结构之单链表非常详细介绍(适合小白)_第39张图片

4.3.11 单链表pos前插入结点 

//单链表结点插入(在pos之前插入)
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	assert(pos);
	assert(pphead);
	//头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		//找到前面一个位置
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuyListNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

数据结构之单链表非常详细介绍(适合小白)_第40张图片

数据结构之单链表非常详细介绍(适合小白)_第41张图片


 4.3.12单链表pos后插入结点 

// 单链表结点插入(在pos之后插入)
void SLTInsertBack(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	assert(pos);
	SLTNode* newnode = BuyListNode(x);
	newnode->next= pos->next;
	pos->next = newnode;
}

数据结构之单链表非常详细介绍(适合小白)_第42张图片

 数据结构之单链表非常详细介绍(适合小白)_第43张图片


4.3.13对 4.3.7~~4.3.12实践检测

我们如何把这几个函数使用起来呢,在初始完链表后,我们可以用SLTNodeFInd函数去找到一个结点(比如说找到某个数据值是4的结点的地址),得到函数返回值——目标结点的地址后,我们可以把它的地址作为pos传给函数,比如说SLTNodeModify函数,或者SLTNodeInsertBack函数,去修改顺序表。

	//删除pos
	SLTNode* pos = SLTNodeFind(plist, 5);
	SLTErase(&plist, pos);
	//删除pos后结点
	SLTNode* pos = SLTNodeFind(plist, 3);
	SLTEraseAfter(&plist,pos);
	//单链表结点修改
	SLTNode* pos = SLTNodeFind(plist, 5);
	SLTModify(plist, pos, 30);
	//在pos之前插入
	SLTNode* pos = SLTNodeFind(plist, 5);
	SLTInsert(&plist,pos,30);
	//在pos之后插入
	SLTNode* pos = SLTNodeFind(plist, 5);
	SLTInsertBack(&plist,pos,30);

	SLTPrint(plist);
	SLTDestory(&plist);

4.3.14销毁链表

销毁链表这一块,咱可不敢直接free(phead),链表在物理结构上是不连续存储的,销毁链表必须要一个结点一个结点去销毁!!!!最后不要忘记把phead置为NULL

//销毁单链表
void SLTDestory(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

五.源代码 

1.SL.h

#pragma once
#include
#include
#include
#include

typedef int SLTDateType;
typedef struct SLTNode
{
	SLTDateType data;
	struct SLTNode* next;
}SLTNode;


//创建一个结点
SLTNode* BuyListNode(SLTDateType x);
//销毁单链表
void SLTDestory(SLTNode** pphead);
//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDateType x);
//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x);
//单链表头删
void SLTPopFront(SLTNode** pphead);
//单链表尾删
void SLTPopBack(SLTNode** pphead);
//单链表结点查找
SLTNode* SLTNodeFind(SLTNode* phead, SLTDateType x);
//单链表结点删除(删除pos位置的结点)
void SLTErase(SLTNode** pphead, SLTNode* pos);
//单链表结点删除(删除pos位置之后的结点)
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos);
//单链表结点插入(在pos之前插入)
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
// 单链表结点插入(在pos之后插入)
void SLTInsertBack(SLTNode** pphead, SLTNode* pos, SLTDateType x);
// 单链表结点修改
void SLTModify(SLTNode* phead, SLTNode* pos, SLTDateType x);
//打印单链表
void SLTPrint(SLTNode* phead);

2.SL.c 

#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
#include"SL.h"


//打印单链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL");
	printf("\n");
}

//销毁单链表
void SLTDestory(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

//创建一个结点
SLTNode* BuyListNode(SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail:");
		return 1;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x)
{
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);
	//链表为空
	//链表不为空
	if (*pphead == NULL)
	{
		*pphead = newnode;
	//不需要让newnode->next=NULL,在BuySLTNode中我们就已经进行过这个操作了
	}
	else
	{
		//先找到链表的尾部
		SLTNode*tail= *pphead;
		while (tail->next!=NULL)
		{
			tail= tail->next;
		}
		tail->next =newnode;
	}
}

//单链表尾删
void SLTPopBack(SLTNode** pphead)
{
	//0.链表为空
	assert(*pphead);
	assert(pphead);
	//1.链表一个结点
	if ((*pphead)->next== NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//2.多个结点
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next!= NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next= NULL;
	}
}

//单链表头删
void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead);
	assert(pphead);
	SLTNode* cur = *pphead;
	*pphead = (*pphead)->next;
	free(cur);
	cur = NULL;
}

//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDateType x)
{
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

//单链表结点查找
SLTNode* SLTNodeFind(SLTNode* phead, SLTDateType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

//单链表结点删除(删除pos位置的结点)
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(*pphead);
	assert(pphead);
	//头删
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	//非头删
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next!= pos)
		{
			prev = prev->next;
			assert(prev->next);
			//这里为什么需要断言
		}
		prev->next = pos->next;
		free(pos);
	}
}

// 单链表结点修改
void SLTModify(SLTNode* phead, SLTNode* pos, SLTDateType x)
{
	assert(pos);
	SLTNode* cur = phead;
	while (cur!=pos)
	{
		cur = cur->next;
		assert(cur);
	}
	pos->data = x;
}

//单链表结点插入(在pos之前插入)
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	assert(pos);
	assert(pphead);
	//头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		//找到前面一个位置
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuyListNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

// 单链表结点插入(在pos之后插入)
void SLTInsertBack(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	assert(pos);
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);
	newnode->next= pos->next;
	pos->next = newnode;
}

//单链表结点删除(删除pos位置之后的结点)
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	assert(pphead);
	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

3.test-SL.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"SL.h"
void Test()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);
	SLTPushBack(&plist, 6);
	/*SLTPrint(plist);
	SLTDestory(&plist);*/
	SLTPushFront(&plist, 40);
	SLTPushFront(&plist, 50);
	/*SLTPrint(plist);
	SLTDestory(&plist);*/
	SLTPopBack(&plist);
	/*SLTPrint(plist);
	SLTDestory(&plist);*/
	SLTPopFront(&plist);
	/*SLTPrint(plist);
	SLTDestory(&plist);*/
	SLTNode* pos = SLTNodeFind(plist,5);
	//删除pos
	SLTNode* pos = SLTNodeFind(plist, 5);
	SLTErase(&plist, pos);
	//删除pos后结点
	SLTNode* pos = SLTNodeFind(plist, 3);
	SLTEraseAfter(&plist,pos);
	//单链表结点修改
	SLTNode* pos = SLTNodeFind(plist, 5);
	SLTModify(plist, pos, 30);
	//在pos之前插入
	SLTNode* pos = SLTNodeFind(plist, 5);
	SLTInsert(&plist,pos,30);
	//在pos之后插入
	SLTNode* pos = SLTNodeFind(plist, 5);
	SLTInsertBack(&plist,pos,30);

	SLTPrint(plist);
	SLTDestory(&plist);
}
int main()
{
	Test();
	return 0;
}

花会沿途盛开,以后也是~

你可能感兴趣的:(数据结构,数据结构,链表,c语言,深度学习)