单链表详细解析|画图理解

news2024/12/24 22:02:48

前言:

        在前面我们学习了顺序表,相当于数据结构的凉菜,今天我们正式开始数据结构的硬菜了,那就是链表,链表有多种结构,但我们实际中最常用的还是无头单向非循环链表和带头双向循环链表,我们今天先学习无头单向循环链表。


1、链表介绍

       1.1链表的概念及结构

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

结点:因为链表的结点在逻辑上是连续,物理上不一定连续,所以每一个结点的类型有两部分组成:数据域(存储链表的数据)和指针域(存储后继结点的地址)。

结构:

①逻辑图:为了方便理解,想象出来的,用形象方式表示(如箭头)

②物理图:内存中真实存储,实实在在数据在内存中的变化

如上两幅图我们可知:①链式结构在逻辑上是连续的,但是在物理上不一定连续;②现实中结点一般都是malloc从堆上申请出来的;③从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。

        1.2顺序表和链表的优缺点

(1)顺序表的缺点:

        ①空间不够了,需要扩容,扩容是要付出代价的。

        ②避免频繁扩容,增容一般都是按倍数去扩(一般2倍适中),可能存在一定空间浪费(如:当前容量为100,满了以后增容到200,我们再继续插入5个数据,后面没有数据插入了,那么就浪费了95个数据空间。)

        ③头部或者中间位置的插入删除,需要挪动,挪到数据也是有消耗的。

(2)顺序表的优点:

        ①支持随机,有些算法需要结构支持随机访问,比如:二分查找,优化的快排等等。

(3)链表的优点:

        ①按需申请空间,不用了就释放空间(更合理的使用了空间)

        ②头部或者中间插入数据,不需要挪动数据

        ③不存在浪费空间

(4)链表的缺点:

        ①每一个结点,都要存一个后继结点的地址去链接后面的数据结点。

        ②不支持随机访问(就是不能使用下标直接访问第i个数据),必须得走0(N)

tip:单链表的缺陷还是有很多的,单纯单链表增删查改的意义不大但是①很多OJ题考察的都是单链表;②单链表更多用于更复杂数据结构的子结构、哈希桶、邻接表等。链表存储数据还要看双向链表,这个后面在学。

根据顺序表和链表的优缺点我们可以看出,链表与顺序表是互补的,相辅相成!

2、单链表(无头单向非循环链表)的实现

        2.1定义单链表结点

        代码演示:

//重定义链表结点中数据域的类型(优点:①见名知意;②一改全改)
typedef int SLTDataType;

//定义链表结点
typedef struct SListNode
{
	SLTDataType data;//用来存放结点的数据
	struct SListNode* next;//用来存放后继结点的地址
}SLTNode;//重命名为SLTNode

        解读:

        ①typedef:类型重命名——作用:见名知意;一改全改;

        ②链表逻辑上是连续,物理上不一定连续,所以它是复杂类型——有两个变量组成,data变量存放结点的数据;next指针变量存放后继结点的地址(这也叫做结构自引用:①注意结构的自引用不能是结构体本身,因为C是自上向下编译的,所以当引用结构本身是结构不完整,报错。②正确的结构自引用是定义成结构体的指针,结构的指针不受结构的内容影响,它只是一个指针,指向你定义的一个结构,至于这个结构完不完整是什么,它都不需要知道。因此编译器能令其通过。)

        2.2单链表的打印模块

因为链表不支持随机访问,所以必须从第一个结点开始依次向后访问。(①我们只需知道头指针即可,所以参数只有一个;②又因为只是打印不用改变链表,所以只需值传递即可。)

        代码演示:

//单链表打印
void SLTPrint(SLTNode* phead)
{
	//assert(phead);//?——错,不用断言,链表为空时也能打印
	//定义一个临时指针变量指向链表的第一个结点
	SLTNode* cur = phead;
	//当链表结点到尾时打印结束:即cur为NULL
	while (cur)
	{
		//打印链表结点的数据
		printf("%d->", cur->data);
		//得到后继结点的地址
		cur = cur->next;
	}
	printf("NULL\n");
}

       调试代码演示:

void SLTNodeText01()
{
	SLTNode* plist = NULL;
	SLTPrint(plist);
}

int main()
{
	SLTNodeText01();
	return 0;
}

         解读:

        ①值传递:形参只是实参的一份临时拷贝,形参的改变不影响实参。

        ②注意:我们不要形成固定思维,看到参数是指针就断言,要根据实际情况判断。图示:

        ③cur = cur->next,为什么就能得到后继结点的地址——结构体指针访问结构体成员可以通过->操作符访问;结点成员next中存储的是后继结点的地址。图示:

        ④我们能不能通过cur++找到下一个结点呢——答案是:不能,因为链表在物理上是不一定连续的。

        2.3单链表的尾插模块

要尾插一个新节点:①我们先创建好一个新节点;②给新节点初始化;③与原链表链接——先找尾,找到后将新节点的地址拷贝给原来的尾结点的next即可。(注意特殊情况:链表为空时,是将新节点的地址拷贝给头指针)

        代码演示:

//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	//创建新节点
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	//判断是否开辟成功
	if (NULL == newnode)
	{
		perror("SLTPushBack::malloc");//打印错误信息
		exit(-1);
	}
	//给新节点初始化
	newnode->data = x;
	newnode->next = NULL;
	//判断是否为空链表
	if (NULL == *pphead)
	{
		//为空链表,直接将新节点的地址拷贝给头指针即可
		*pphead = newnode;
	}
	else
	{
		//不为空链表,找到原尾结点,将新尾节点的地址拷贝给原尾结点的next
		/*
		* 错误代码演示:
		SLTNode* tail = *pphead;//定义一个局部变量指针
		while (tail)
		{
			tail = tail->next;
		}//将局部变量指针赋值为空指针
		tail = newnode;//再将局部变量指针指向新节点
		*/
		SLTNode* tail = *pphead;//定义一个局部变量指针
		while (tail->next != NULL)
		{
			tail = tail->next;
		}//找到尾结点
		tail->next = newnode;//将新尾结点的地址拷贝给原尾结点的next
	}
}

        调试代码演示:

void SLTNodeText02()
{
	SLTNode* plist = NULL;//头指针指向链表的第一个结点

	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);

	//打印
	SLTPrint(plist);
}

int main()
{
	SLTNodeText02();
	return 0;
}

        运行结果:

        解读:

        ①链表是按需申请空间的,所以malloc在堆区申请空间——使用malloc申请空间需注意:malloc申请的空间没有初始化,使用前要初始化;malloc创建失败会返回NULL,使用前要判断是否开辟成功(一般都能开辟成功);malloc申请空间传的大小单位是字节;图示:

        ②给新节点初始化——结构体指针访问成员使用操作符->;因为是尾插入的新节点,所以next为空。

        ③与原尾结点链接——非空链表:链接就是将原尾结点的next指向新节点;空链表:链接只需将头指针指向新节点即可(形参要影响实参,需要传实参的地址);

        ④因为形参的改变要影响实参,所以是传址调用——传实参的地址,在函数中可以通过解引用去改变实参,要改变什么类型的值,就传什么类型的指针(如实参是int,就传int*的指针)。

        2.4单链表的头插模块&创建新节点模块

新节点模块:因为我们每一次插入数据时,都要创建新节点,操作重复,所以我们可以将其封装为一个单独的模块。

        创建新节点代码演示:

//创建新节点
SLTNode* BuySLTNode(SLTDataType x)
{
	//创建新节点
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	//判断是否开辟成功
	if (NULL == newnode)
	{
		perror("SLTPushBack::malloc");//打印错误信息
		exit(-1);
	}
	//给新节点初始化
	newnode->data = x;
	newnode->next = NULL;
	//返回新节点的地址
	return newnode;
}

        有了该模块,以后我们需要创建新节点,直接调用即可。

头插模块:头插我们创建好新节点后,我们只需将其链接起来即可——新节点先指向原来的第一个结点,再头指针指向新节点。

        头插的代码演示:

//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	//创建新节点
	SLTNode* newnode = BuySLTNode(x);
	//创建链接
	//①新节点指向原第一个结点
	newnode->next = *pphead;
	//②头指针指向新节点
	*pphead = newnode;
}

        测试代码:

void SLTNodeText03()
{
	SLTNode* plist = NULL;//头指针指向链表的第一个结点

	//头插
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);

	//打印
	SLTPrint(plist);
}

int main()
{
	SLTNodeText03();
	return 0;
}

        运行结果:

        解读:

        ①只要是要求形参的改变要影响实参,就需要址传递——传实参的地址,在函数中可以通过解引用去改变实参,要改变什么类型的值,就传什么类型的指针;

        ②链接(空链表和非空链表的方式一样),图示:

        2.5单链表的尾删

单链表的尾删有三种情况:

        情况一:空链表——处理方法①温柔的方式:使用if语句判断,如果为空则直接退出;②严格的方式:断言(为空则直接报错)。

        情况二:只有一个结点——①free释放结点(注:free释放完之后的指向动态内存开辟的空间的指针变量不会改变,仍能找到那块空间,有危险(可能非法访问),所以使用完free之后一定记得将其赋值为NULL。);②将头指针置为NULL。图示:

        情况三:多个结点——尾删掉最后一个,并且要将原来的倒数第二个节点next置为NULL。图示:

        常见性错误:只是释放了尾结点

如图:我们可知问题——可以找到尾结点,但是尾结点的前一位找不到,无法置为空。

        解决方案1:双指针——tail指针找原尾结点,prev指针存储原尾结点的前一个结点地址。

        解决方案2:找倒数第二个结点——当tail->next->next为NULL时即找到。

        尾删代码演示:

//单链表尾删
void SLTPopBack(SLTNode** pphead)
{
	/*
	* 错误代码
	SLTNode* tail = *pphead;
	//找尾结点
	while (tail->next)
	{
		tail = tail->next;
	}
	//释放尾结点
	free(tail);
	tail = NULL;
	*/
	//二级指针不可能为空
	assert(pphead);
	//1、链表为空
	assert(*pphead);
	//2、只有一个结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		//free只是将该申请的空间还给了操作系统,并没有改变其值,所以将其置空
		*pphead = NULL;
	}
	//3、多个结点
	else
	{
		双指针法:
		//SLTNode* prev = NULL;
		//SLTNode* tail = *pphead;
		//while (tail->next)
		//{
		//	prev = tail;
		//	tail = tail->next;
		//}
		//free(tail);
		//tail = NULL;
		//prev->next = NULL;

		//找倒数第二个结点
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

        测试代码:

void SLTNodeText03()
{
	SLTNode* plist = NULL;//头指针指向链表的第一个结点

	//头插
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);

	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPrint(plist);

}

int main()
{
	SLTNodeText03();
	return 0;
}

        运行结果:

        2.6单链表的头删

头删就比较简单了,只分为两种情况:

        情况1:空链表——直接断言即可(删除就像消费,所以和没钱就不能买都一样)

        情况2:非空链表——注意要先将头指针指向第二个结点的地址,再将释放头结点。

        头删代码演示:

//单链表头删
void SLTPopFront(SLTNode** pphead)
{
	//二级指针一定不为空
	assert(pphead);
	//断言是否为空链表
	assert(*pphead);
	//1、修改头指针:使之指向第二个结点
	SLTNode* first = *pphead;//保存头结点的地址
	*pphead = first->next;
	//2、释放头结点
	free(first);
}

        测试代码:

void SLTNodeText04()
{
	SLTNode* plist = NULL;//头指针指向链表的第一个结点

	//头插
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);

	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	/*SLTPopFront(&plist);
	SLTPrint(plist);*/

}

int main()
{
	SLTNodeText04();
	return 0;
}

        运行结果:

        2.7单链表的查找(修改)

查找:我们只需从头开始遍历链表即可,找到即返回结点的地址,找不到返回空。(查找同时也代表修改功能:我们找到了结点,就会返回结点的地址,通过结点的地址,我们就可以修改了)

        查找(修改)代码演示:

//单链表的查找(修改)
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	//遍历链表
	while (cur)
	{
		//找到返回该节点地址,找不到向后遍历
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	//找不到返回空
	return NULL;
}

        测试代码:

void SLTNodeText05()
{
	SLTNode* plist = NULL;//头指针指向链表的第一个结点

	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 2);

	//打印
	SLTPrint(plist);

	//找到值2的结点将其修改为9
	SLTNode* ret = SLTFind(plist, 2);
	while (ret)
	{
		ret->data = 9;
		ret = SLTFind(ret, 2);
	}

	SLTPrint(plist);
}


int main()
{
	SLTNodeText05();
	return 0;
}

        运行结果:

        2.8单链表指定在pos位置之前插入VS在pos位置之后插入

pos位置之前插入:分两种情况:

        情况一:pos指向头结点——即头插

        情况二:pos不指向头结点——在pos前插入数据,就要找到pos的前一个结点位置(有效率损失)。

        pos之前插入代码演示:

//单链表指定pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);//pos不能为空(要我们在pos位置插入,那pos位置就得存在)
	assert(pphead);
	if (*pphead == pos)
	{
		SLTPushFront(pphead, x);
	}//pos指向头结点,在前插入相当于头插
	else
	{
		SLTNode* prev = *pphead;//保存pos前一个结点的地址
		while (prev->next != pos)
		{
			prev = prev->next;
		}//找到pos前一个结点
		SLTNode* newnode = BuySLTNode(x);
		//新节点与原链表链接
		prev->next = newnode;
		newnode->next = pos;
	}//pos不指向头结点
}

        测试代码:

void SLTNodeText06()
{
	SLTNode* plist = NULL;//头指针指向链表的第一个结点

	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	
	//打印
	SLTPrint(plist);

	//找到值2的结点在其前面插入6
	SLTNode* ret = SLTFind(plist, 2);
	SLTInsert(&plist, ret, 6);

	SLTPrint(plist);
}

int main()
{
	SLTNodeText06();
	return 0;
}

        运行结果:

pos位置之后插入:pos指向头结点和不指向头结点操作都是一样的——创建好新节点之后,只是需要注意新节点先与pos的后继结点链接,再与pos位置结点链接。

        pos位置之后插入代码演示:

//单链表指定pos位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	//创建新节点
	SLTNode* newnode = BuySLTNode(x);
	//链接
	//1、新节点先与pos的后继结点链接
	newnode->next = pos->next;
	//2、新节点再与pos链接
	pos->next = newnode;
}

        测试代码:

void SLTNodeText07()
{
	SLTNode* plist = NULL;//头指针指向链表的第一个结点

	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);

	//打印
	SLTPrint(plist);

	//找到值2的结点在其之后插入6
	SLTNode* ret = SLTFind(plist, 2);
	SLTInsertAfter(ret, 6);

	SLTPrint(plist);
}

int main()
{
	SLTNodeText07();
	return 0;
}

        运行结果:

        两种插入方式,那种更好呢?

        ①pos位置之前插入需要分两种情况;pos位置之后插入不用分情况,都一样。

        ②pos位置之前插入时间复杂度为O(N)——因为之前插入需要找pos的前趋结点位置才能将新节点与链表链接起来;pos位置之后插入时间复杂度为0(1)。

        终上,我们链表选择在pos位置之后插入更好。

        2.9单链表的pos位置删除VSpos位置之后删除

pos位置删除:分两种情况:

        情况一:pos指向链表的第一个结点——即头删

        情况二:pos不指向链表的头结点——①找到pos的前驱结点;②pos的前驱结点先与pos的后继结点链接好了,再释放pos位置结点(因为先释放pos结点就找不到pos的后继结点了)。

注意:该函数只是将pos位置释放了,并没有将pos指向改变,记得在主调函数置为空,防止非法访问。

        pos位置删除代码演示:

//单链表pos位置删除(注意该函数只将pos位置空间释放,并没有将其置为空)
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);//pos位置不能为空(pos位置不为空,链表也不为空)

	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}//pos指向链表头结点
	else
	{
		SLTNode* prev = *pphead;//prev储存pos前一个结点位置
		while (prev->next != pos)
		{
			prev = prev->next;
		}//找到pos的前一个结点
		//1、先将pos的前驱结点和后继结点链接
		prev->next = pos->next;
		//2、再将pos位置结点释放
		free(pos);
	}
}

        测试代码:

void SLTNodeText08()
{
	SLTNode* plist = NULL;//头指针指向链表的第一个结点

	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);

	//打印
	SLTPrint(plist);

	//找到值2的结点再将其删除
	SLTNode* ret = SLTFind(plist, 2);
	SLTErase(&plist, ret);
	ret = NULL;

	SLTPrint(plist);
}


int main()
{
	SLTNodeText08();
	return 0;
}

        运行结果:

pos位置之后删除:pos指向头结点和不指向头结点操作都一样,图示:

        pos之后删除代码演示:

//单链表pos位置之后删除
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);

	SLTNode* del = pos->next;//保存pos后继结点的位置
	pos->next = del->next;//链接
	free(del);//释放
	del = NULL;//free不会改变del的内容
}

        测试代码:

void SLTNodeText09()
{
	SLTNode* plist = NULL;//头指针指向链表的第一个结点

	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);

	//打印
	SLTPrint(plist);

	//找到值2的结点再将其后继结点删除
	SLTNode* ret = SLTFind(plist, 2);
	SLTEraseAfter(ret);

	SLTPrint(plist);
}

int main()
{
	SLTNodeText09();
	return 0;
}

        运行结果:

        pos位置删除与pos位置之后删除,谁更好呢?

        答案是:pos位置之后删除更好,因为①pos位置之后不用分情况更简单;②pos位置之后删除不用找pos的前驱结点,效率更高。pos位置删除时间复杂度为O(N),之后删除时间复杂度为O(1).

        2.10单链表的销毁

注意单链表的销毁不是将头指针free就销毁了,是从头结点开始逐个销毁。

注意:

        ①free释放动态开辟的内存;

        ②free释放只是是将指针指向的那块动态内存还给操作系统,但是指针仍指向那块空间(为野指针),在使用指针就会造成非法访问,所以free之后记得一般将其置为空。

        销毁代码演示:

//单链表的销毁
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	//从头结点逐个将其释放
	while (cur)
	{
		//保存cur下一个结点的地址,因为free掉cur后内存还给操作系统了
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	//销毁完之后将头结点置为空
	*pphead = NULL;
}

        测试代码:

void SLTNodeText10()
{
	SLTNode* plist = NULL;//头指针指向链表的第一个结点

	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);

	//打印
	SLTPrint(plist);

	//销毁链表
	SLTDestroy(&plist);

	SLTPrint(plist);
}

int main()
{
	SLTNodeText10();
	return 0;
}

        运行结果:

3、总代码

        3.1单链表的声明模块:SList.h

//预处理:包含,后续常用的头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

//重定义链表结点中数据域的类型(优点:①见名知意;②一改全改)
typedef int SLTDataType;

//定义链表结点
typedef struct SListNode
{
	SLTDataType data;//用来存放结点的数据
	struct SListNode* next;//用来存放后继结点的地址
}SLTNode;//重命名为SLTNode

//单链表打印
void SLTPrint(SLTNode* phead);

//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);

//创建新节点
SLTNode* BuySLTNode(SLTDataType x);

//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);

//单链表尾删
void SLTPopBack(SLTNode** pphead);

//单链表头删
void SLTPopFront(SLTNode** pphead);

//单链表的查找(修改)
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

//单链表指定pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

//单链表指定pos位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

//单链表pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos);

//单链表pos位置之后删除
void SLTEraseAfter(SLTNode* pos);

//单链表的销毁
void SLTDestroy(SLTNode** pphead);

        3.2单链表的实现模块:SList.c

#include"SList.h"

//单链表打印
void SLTPrint(SLTNode* phead)
{
	//assert(phead);//?——错,不用断言,链表为空时也能打印
	//定义一个临时指针变量指向链表的第一个结点
	SLTNode* cur = phead;
	//当链表结点到尾时打印结束:即cur为NULL
	while (cur)
	{
		//打印链表结点的数据
		printf("%d->", cur->data);
		//得到后继结点的地址
		cur = cur->next;
	}
	printf("NULL\n");
}

//创建新节点
SLTNode* BuySLTNode(SLTDataType x)
{
	//创建新节点
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	//判断是否开辟成功
	if (NULL == newnode)
	{
		perror("SLTPushBack::malloc");//打印错误信息
		exit(-1);
	}
	//给新节点初始化
	newnode->data = x;
	newnode->next = NULL;
	//返回新节点的地址
	return newnode;
}

//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	创建新节点
	//SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	判断是否开辟成功
	//if (NULL == newnode)
	//{
	//	perror("SLTPushBack::malloc");//打印错误信息
	//	exit(-1);
	//}
	给新节点初始化
	//newnode->data = x;
	//newnode->next = NULL;
	// 
	//二级指针一定不为空
	assert(pphead);
	//创建新节点
	SLTNode* newnode = BuySLTNode(x);
	//判断是否为空链表
	if (NULL == *pphead)
	{
		//为空链表,直接将新节点的地址拷贝给头指针即可
		*pphead = newnode;
	}
	else
	{
		//不为空链表,找到原尾结点,将新尾节点的地址拷贝给原尾结点的next
		/*
		* 错误代码演示:
		SLTNode* tail = *pphead;//定义一个局部变量指针
		while (tail)
		{
			tail = tail->next;
		}//将局部变量指针赋值为空指针
		tail = newnode;//再将局部变量指针指向新节点
		*/
		SLTNode* tail = *pphead;//定义一个局部变量指针
		while (tail->next != NULL)
		{
			tail = tail->next;
		}//找到尾结点
		tail->next = newnode;//将新尾结点的地址拷贝给原尾结点的next
	}
}

//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	//二级指针一定不为空
	assert(pphead);
	//创建新节点
	SLTNode* newnode = BuySLTNode(x);
	//创建链接
	//①新节点指向原第一个结点
	newnode->next = *pphead;
	//②头指针指向新节点
	*pphead = newnode;
}

//单链表尾删
void SLTPopBack(SLTNode** pphead)
{
	/*
	* 错误代码
	SLTNode* tail = *pphead;
	//找尾结点
	while (tail->next)
	{
		tail = tail->next;
	}
	//释放尾结点
	free(tail);
	tail = NULL;
	*/
	//二级指针不可能为空
	assert(pphead);
	//1、链表为空
	assert(*pphead);
	//2、只有一个结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		//free只是将该申请的空间还给了操作系统,并没有改变其值,所以将其置空
		*pphead = NULL;
	}
	//3、多个结点
	else
	{
		双指针法:
		//SLTNode* prev = NULL;
		//SLTNode* tail = *pphead;
		//while (tail->next)
		//{
		//	prev = tail;
		//	tail = tail->next;
		//}
		//free(tail);
		//tail = NULL;
		//prev->next = NULL;

		//找倒数第二个结点
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

//单链表头删
void SLTPopFront(SLTNode** pphead)
{
	//二级指针一定不为空
	assert(pphead);
	//断言是否为空链表
	assert(*pphead);
	//1、修改头指针:使之指向第二个结点
	SLTNode* first = *pphead;//保存头结点的地址
	*pphead = first->next;
	//2、释放头结点
	free(first);
}

//单链表的查找(修改)
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	//遍历链表
	while (cur)
	{
		//找到返回该节点地址,找不到向后遍历
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	//找不到返回空
	return NULL;
}


//单链表指定pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);//pos不能为空(要我们在pos位置插入,那pos位置就得存在)
	assert(pphead);
	if (*pphead == pos)
	{
		SLTPushFront(pphead, x);
	}//pos指向头结点,在前插入相当于头插
	else
	{
		SLTNode* prev = *pphead;//保存pos前一个结点的地址
		while (prev->next != pos)
		{
			prev = prev->next;
		}//找到pos前一个结点
		SLTNode* newnode = BuySLTNode(x);
		//新节点与原链表链接
		prev->next = newnode;
		newnode->next = pos;
	}//pos不指向头结点
}

//单链表指定pos位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	//创建新节点
	SLTNode* newnode = BuySLTNode(x);
	//链接
	//1、新节点先与pos的后继结点链接
	newnode->next = pos->next;
	//2、新节点再与pos链接
	pos->next = newnode;
}


//单链表pos位置删除(注意该函数只将pos位置空间释放,并没有将其置为空)
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);//pos位置不能为空(pos位置不为空,链表也不为空)

	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}//pos指向链表头结点
	else
	{
		SLTNode* prev = *pphead;//prev储存pos前一个结点位置
		while (prev->next != pos)
		{
			prev = prev->next;
		}//找到pos的前一个结点
		//1、先将pos的前驱结点和后继结点链接
		prev->next = pos->next;
		//2、再将pos位置结点释放
		free(pos);
	}
}

//单链表pos位置之后删除
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);

	SLTNode* del = pos->next;//保存pos后继结点的位置
	pos->next = del->next;//链接
	free(del);//释放
	del = NULL;//free不会改变del的内容
}

//单链表的销毁
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	//从头结点逐个将其释放
	while (cur)
	{
		//保存cur下一个结点的地址,因为free掉cur后内存还给操作系统了
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	//销毁完之后将头结点置为空
	*pphead = NULL;
}

        今天我们就学完了无头单向非循环链表,下期更新带头双向循环链表,作者水平有限,希望大家多多支持。

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

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

相关文章

删除表

MySQL从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129334507?spm1001.2014.3001.5502 语法格式: drop table 表名; 说明:连同表结构、表中的数据都删除 案例&#xff1a;删除test03表&#xff0c;并验证 mysql> desc test03; ---…

安装Pymc3模块包问题记录

首先跟着各个方法安装&#xff0c;都不行&#xff0c;导入pymc3包时&#xff0c;就会报各种错&#xff1b;最后找了好几个博客跟着修改&#xff0c;最终才把pymc3包安装上了&#xff0c;也能导入进去了。 重新整理下安装步骤&#xff1a; 1、下载安装Anaconda3&#xff1a; …

【趣味JavaScript】5年前端开发都没有搞懂toString和valueOf这两个方法!

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;web开发者、设计师、技术分享博主 &#x1f40b; 希望大家多多支持一下, 我们一起进步&#xff01;&#x1f604; &#x1f3c5; 如果文章对你有帮助的话&#xff0c;欢迎评论 &#x1f4ac;点赞&#x1…

在C或C++中查找内存泄漏

编程软件中的内存泄漏可能很难精确定位&#xff0c;因为这里面有大量的数据。本文中&#xff0c;您可以学习如何借助运行时错误检测工具查找C和C应用程序中的内存泄漏。 什么是内存泄漏&#xff1f;C和C语言实例 What Is a Memory Leak? 当您面临内存泄漏时&#xff0c;C和C…

师德师风演讲稿写作格式:如何用三句话吸引听众的注意力

写师德师风演讲稿时&#xff0c;可以按照以下格式进行写作&#xff1a; 1. 开头部分&#xff1a; a. 引起听众的兴趣&#xff0c;可以使用一个引人入胜的故事、一个有趣的事实或者一个引人思考的问题。 b. 简要介绍自己以及演讲的主题。 2. 主体部分&#xff1a; a. 阐述师…

多维时序 | MATLAB实现GWO-BP多变量时间序列预测(灰狼算法优化BP神经网络)

多维时序 | MATLAB实现GWO-BP多变量时间序列预测(灰狼算法优化BP神经网络) 目录 多维时序 | MATLAB实现GWO-BP多变量时间序列预测(灰狼算法优化BP神经网络)效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.MATLAB实现GWO-BP多变量时间序列预测(灰狼算法优化BP神经网络)&…

华为云云耀云服务器L实例评测 | 云服务器搭建自己的gitlab代码仓库手把手教学

&#x1f4cb; 前言 &#x1f5b1; 博客主页&#xff1a;在下马农的碎碎念&#x1f917; 欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;✍ 本文由在下马农原创&#xff0c;首发于CSDN&#x1f4c6; 首发时间&#xff1a;2023/09/26&#x1f4c5; 最近更新时…

crypto:Quoted-printable

题目 解压文件后可得到提示文本 好了这个没接触过&#xff0c;参考别的大佬wp QP为可打印字符编码&#xff0c;根据加密方式任何一个8位的字节值可编码为3个字符&#xff1a;一个等号“”后跟随两个十六进制数字&#xff08;0–9或A–F&#xff09;表示该字节的数值。 利用网…

完全背包 动态规划 + 一维dp数组

动态规划&#xff1a;完全背包理论基础 每件商品都有无限个&#xff01;&#xff01;&#xff01; &#xff08;1&#xff09;0-1背包的核心代码 解决0-1背包问题&#xff08;方案二&#xff09;&#xff1a;一维dp数组&#xff08;滚动数组&#xff09;_呵呵哒(&#xffe3;…

CodeWhisperer,非常丝滑的AI代码神器

文章目录 什么是 Amazon CodeWhisperer&#xff1f;快速上手CodeWhisperer安装配置如何使用 Amazon CodeWhispererCodeWhisperer初体验&#xff1a;hello world Python语言快速入门向文件写入数据读取csv文件排序算法之冒泡排序设计模式之单例模式 使用CodeWhisperer快速上手Py…

这本书竟然把JAVA讲的如此透彻!漫画JAVA火爆出圈!

亲爱的粉丝们&#xff0c;你是否曾经为学习JAVA而苦恼&#xff1f;繁复的代码和复杂的逻辑常常让人感到头大。不过&#xff0c;今天我要为大家介绍一本神奇的书——《漫画JAVA》&#xff0c;它以图文并茂的方式&#xff0c;轻松诙谐地讲解了JAVA的方方面面。在这篇文章中&#…

基于.Net Core实现自定义皮肤WidForm窗口

前言 今天一起来实现基于.Net Core、Windows Form实现自定义窗口皮肤&#xff0c;并实现窗口移动功能。 素材 准备素材&#xff1a;边框、标题栏、关闭按钮图标。 窗体设计 1、创建Window窗体项目 2、窗体设计 拖拉4个Panel控件&#xff0c;分别用于&#xff1a;标题栏、关…

LabVIEW开发实时自动化多物镜云计算全玻片成像装置

LabVIEW开发实时自动化多物镜云计算全玻片成像装置 数字病理学领域正在迅速发展&#xff0c;这主要是由于计算机处理能力、数据传输速度、软件创新和云存储解决方案方面的技术进步。因此&#xff0c;病理科室不仅将数字成像用于图像存档等简单任务&#xff0c;还用于远程病理学…

无菌生产使用的纯蒸汽质量检测必要性及验证服务

纯蒸汽常被用于制药行业的无菌生产中。无菌生产所用到的物料、容器、设备等物品需要使用纯蒸汽进行湿热灭菌处理。纯蒸汽的主要检测指标&#xff0c;如微生物限度、电导率、TOC等应满足《中华人民共和国药典》中注射用水的质量指标规定。 当纯蒸汽用于湿热灭菌时&#xff0c;为…

【MySQL】开启 canal同步MySQL增量数据到ES

开启 canal同步MySQL增量数据到ES canal 是阿里知名的开源项目&#xff0c;主要用途是基于 MySQL 数据库增量日志解析&#xff0c;提供增量数据订阅和消费。示使用 canal 将 MySQL 增量数据同步到ES。 一、集群模式 图中 server 对应一个 canal 运行实例 &#xff0c;对应一…

C++刷题 全排列问题

C刷题 全排列问题 题目描述思路讲解代码展示 题目描述 思路讲解 代码展示 #include <iostream>using namespace std;const int maxn 11;//P为当前排列&#xff0c;hashTable记录整数x是否已经在P中 int n, P[maxn], hashTable[maxn] {false};//当前处理排列的第index号…

Mysql高级——数据库设计规范(2)

8. ER模型 ER 模型中有三个要素&#xff0c;分别是实体、属性和关系。 实体&#xff0c;可以看做是数据对象&#xff0c;往往对应于现实生活中的真实存在的个体。在 ER 模型中&#xff0c;用矩形来表示。实体分为两类&#xff0c;分别是强实体和弱实体。强实体是指不依赖于其…

ElementUI动态树,数据表格以及分页的实现

目录 前言 一. ElementUI动态树 二. 数据表格和分页 三. 后端代码 service层 controller层 前言 在上一篇博客中实现了左侧菜单栏&#xff0c;在此基础上将它变为动态的&#xff0c;即动态的展示数据库的数据。还有数据表格的实现以及分页。&#xff08;纯代码分享&#…

Opengl之基础光照

现实世界的光照是极其复杂的&#xff0c;而且会受到诸多因素的影响&#xff0c;这是我们有限的计算能力所无法模拟的。因此OpenGL的光照使用的是简化的模型&#xff0c;对现实的情况进行近似&#xff0c;这样处理起来会更容易一些&#xff0c;而且看起来也差不多一样。这些光照…

【DenseNet模型】

【DenseNet模型】 1 DenseNet结构2 DenseNet特征图保持一致方法3 模型预览方法 1 DenseNet结构 参考链接&#xff1a;https://arxiv.org/pdf/1608.06993.pdf DenseNet通过密集连接&#xff0c;可以缓解梯度消失问题&#xff0c;加强特征传播&#xff0c;鼓励特征复用&#xff0…