003 数据结构_无头单向非循环链表的详细分解——“C”

引入

前言

本文介绍的是无头单向非循环链表,这种链表结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

链表是什么

常见的链表包括:

单向链表(singly linked list):单向链表是最基本的链表形式,每个节点只有一个指针指向它的后继节点。

双向链表(doubly linked list):双向链表在单向链表的基础上增加了一个指针,使得每个节点不仅可以访问它的后继节点,也可以访问它的前驱节点。

循环链表(circular linked list):循环链表与单向或双向链表相似,唯一的区别在于,链表的最后一个节点指向第一个节点,从而形成了一个循环。

无头链表(headless linked list):无头链表是一种特殊的链表结构,它没有头节点,直接将链表的第一个元素作为起始节点。

链表中有环的问题(linked list cycle problem):这不是一种特定的链表类型,而是一种经典的算法问题。给定一个链表,判断其中是否存在环的问题,通常采用快慢指针的技巧来解决。

什么是单链表

单链表(singly linked list)是一种常用的数据结构,它由多个节点(node)构成。每个节点都有一个存储的值和一个指向下一个节点的指针(next)。通常用头结点来表示整个单链表。

什么是节点

节点(node),是指单链表中的每个元素,它包含两个部分:存储数据元素的数据域和一个指向下一个节点的指针(next)。在单链表中,第一个节点称为头节点(head node),它不存储任何数据元素,只有一个指向第一个真正存储数据元素的节点的指针。最后一个节点称为尾节点(tail node),它的指针指向 null,表示链表结束。
我们通常用一个结构体来表示节点

如下:

typedef int SLTDateType;  //重命名int类型名字
typedef struct SListNode  
{
	SLTDateType data; 	  //存储当前节点的数据
	struct SListNode* next;	//指向下一个下一个节点的指针
}SListNode;

单链表的基本功能

单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
在任意位置后插入
void SListInsertAfter(SListNode* pos, SLTDateType x);
在任意位置前插入
void SListInsert(SListNode** pphead, SListNode* pos, SLTDateType x);

单链表的尾删
void SListPopBack(SListNode** pplist);
单链表头删
void SListPopFront(SListNode** pplist);
在任意位置后删除
// 分析思考为什么不删除pos位置?
void SListEraseAfter(SListNode* pos);

查找
SListNode* SListFind(SListNode* plist, SLTDateType x);

		pos = SListFind(&plist, 70); 			//查找
		pos->data = 30;							//修改

话不多说——>开肝

步骤分解

扩容


//购买节点
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}


插入

头插

// 单链表头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{	
	SListNode* newnode = BuySListNode(x);//购买节点
	//第一种情况
	if(*pplist == NULL)  //里面无节点
	{
		*pplist = newnode;	//把节点赋给空节点,*pplist作为第一个节点	
	}
	//第二种情况
	else
	{
	newnode->next = *pplist;  //让newnode成为第一个节点,链接下一个节点*pplist
	*pplist = newnode;//把newnode后面所有的节点赋给*pplist
	}
}

解析图解:分为两种情况。一般我们先想到的是第二种,即一开始单链表内不为空,存在着节点
即第二种情况下
在这里插入图片描述
newnode->next = *pplist
在这里插入图片描述
*pplist = newnode
解析:注意这句代码的理解,把newnode节点赋给头节点,由于当单链表不为空时,会执行newnode->next = *pplist此语句,让newnode节点链接头节点,因此我们每一次头插完后都应该让头插后的 *pplist成为头节点
让 *pplist表示单链表

在这里插入图片描述
执行 *pplist = newnode
在这里插入图片描述

尾插

// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{	
	SListNode* newnode = BuySListNode(x);
	//第一种情况
	if (*pplist == NULL)
	{
		*pplist = newnode;
		return;
	}
	//SListNode* phead = pplist; 
	//
	//改变结构体,使用结构体指针,把&pplist地址传给phead
	SListNode* phead = *pplist;  //由于用pplist二级指针
	//理解:phead指针等于*pplist指针,地址也相同,之前所说的通过修改*phead的值影响不到*pplist是错误的
	// 把一个值传给形参,通过修改形参妄想修改到实参的值,但形参一出作用域就销毁了,因此这里说传址
	//第二种情况
	while (phead->next != NULL)
	{
		phead = phead->next;	//找到phead->NULL
	}
	phead->next = newnode;      //链接节点
}

解析图解:尾插也分为两种情况,第一种为单链表为空时的尾插
第二种为单链表不为空时的尾插,一般下我们先考虑第二种情况
1、找到尾节点
phead = phead->next;
在这里插入图片描述
相当于遍历单链表,找到尾节点
2、链接尾节点和newnod节点
phead->next = newnode;

在任意位置的前方插入

//任意pos位置的前插
void SListInsert(SListNode**pphead, SListNode* pos, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);  //扩容,购买节点
	SListNode* prev=*pphead; 
	SListNode* cur = NULL;
	if (*pphead == pos)
	{
		SListPushFront(pphead,x);
	}
	else    //problem!!!!
	{
		while (prev != pos)  //遍历找到prev
		{
			cur = prev;   //保存prev移动前:第一个节点的位置
			prev = prev->next;
		}
		newnode->next = pos;  //链接newnode和pos节点
		cur->next = newnode; //链接prev与newnode节点
	}
}

解析图解:分为两种情况,第一种是单链表中只存在一个节点,我们在此节点前插入一个newnode节点,这里复用头插即可。
第二种情况是单链表中存在两个及以上的节点,我们需要找到任意位置处节点前一个节点,和任意位置处的后一个节点
在这里插入图片描述
PS:这里经常会出现一个问题

	while (prev != pos)  //遍历找到prev
		{
			cur = prev;   //保存prev移动前:第一个节点的位置
			prev = prev->next;
		}

我们看到这个循环语句,这句cur = prev; 是一定不能忘记的
由于prev != pos,prev会一直找到pos节点后停止,如果不记录prev找到pos节点前的位置,那么当prev != pos,会执行prev = prev->next;。当下一个节点就是pos的话,此时pos节点是等于prev节点的
当执行下面语句时
newnode->next = pos; 链接newnode和pos节点
cur->next = newnode; 链接prev与newnode节点
会出现死循环的情况,即自己链接自己
在这里插入图片描述

在这里插入图片描述

在任意位置的后方插入

//任意pos位置向后插入
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);  //扩容,购买节点
	newnode->next = pos->next;  //链接newnode与pos的下一个节点
	pos->next = newnode;		//链接pos与newnode节点
	//ps:这里不能先链接pos与newnode节点,否则pos->next(pos的下一个节点没有办法表示)直接丢失了链接
}

解析图解

在这里插入图片描述

删除

头删


// 单链表头删
void SListPopFront(SListNode** pplist)
{

	assert(pplist);     // 确保地址没有传错
	assert(*pplist);    //节点为空,删什么删
	SListNode* del = *pplist;  //让del节点记录头节点
	*pplist = del->next;
}

解析图解
在这里插入图片描述

尾删

// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(pplist);     // 确保地址没有传错
	assert(*pplist);   //节点不为空,删什么删
	SListNode* del = *pplist;
	SListNode* cache = NULL;
	while (del->next!= NULL)  //找尾节点
	{
		cache = del;    //记录尾节点前一个节点
		del = del->next;
	}
	cache->next = NULL;   //置空尾节点前一个节点的next
	//否则等到打印plist->next,next已经被释放了,访问野指针
	free(del);	    //释放尾节点内存
}

解析图解
在这里插入图片描述

删除任意位置后方的节点

// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode** pphead, SListNode* pos)
{
	SListNode* prev = *pphead; 
	while (prev != pos)  //遍历找到prev
	{
		prev = prev->next;
	}
	prev->next = pos->next->next;
}

解析图解
在这里插入图片描述

查找

//单链表查找
SListNode* SListFind(SListNode** pplist, SLTDateType x)
{
	assert(*pplist);    //为空,查什么查
	assert(pplist);		//检查是否传错
	SListNode* phead = *pplist;
	if (phead->data == x)
	{
		return phead;
	}
	while (phead->data != x)
	{
		phead = phead->next;   
	}
	return phead;
}

打印

// 单链表打印
void SListPrint(SListNode* plist)
{
	assert(plist);
	//SListNode* phead = plist;
	while (plist->next!= NULL)
	{
		printf("%d->", plist->data); //当找到最后一个节点后,最后一个节点也要打印
		plist = plist->next; //遍历
	}
	printf("%d->NULL\n", plist->data);
}

销毁

在使用完malloc()分配的内存后,应该释放(deallocating)它以免发生内存泄漏(memory leak)。
内存泄漏指的是程序申请了一段内存,但在不再使用时却没有及时释放这段内存,导致这部分内存永远无法被别的程序或操作系统回收利用。随着内存泄漏不断发生,程序运行时可用的内存会越来越少,直到最终耗尽所有可用内存,导致程序崩溃或系统故障。

为了避免这种情况发生,必须使用free()函数将malloc()分配的内存释放掉。这将把已分配的内存标记为可重用,可以提供给其他部分使用。同时,销毁了malloc()分配的内存后,应该将与之关联的指针设置为空指针,以确保不会在后续的代码中引用未知的内存地址。

void SListDestroy(SListNode** plist)
{
	assert(*plist);
	SListNode* cur = *plist;		  
	while (cur)						  //边迭代边释放
	{
		SListNode* next = cur->next;  //保存下一个节点
		free(cur);
		cur = next;
	}
	*plist = NULL;        //确保不会在后续的代码中引用未知的内存地址
}

通过监视可以观察到cur指向的当前节点已经被释放
在这里插入图片描述
总之,当我们使用malloc()函数分配内存时,就要在合适的时机使用free()函数销毁分配的内存,并在此之后及时将与之关联的指针置为空。这样可以避免内存泄漏,提高程序运行效率和可靠性。

测试用例

void test1()
{
	SListNode* plist = NULL;	//一开始为空
	SListPushBack(&plist, 1);	//尾插
	SListPushBack(&plist, 2);	//尾插
	SListPushBack(&plist, 3);	//尾插
	SListPushFront(&plist, 30);	//头插
	SListPopFront(&plist);		//头删
	SListPopBack(&plist);		//尾删
	SListPrint(plist);			//打印
	SListPopBack(&plist);		//尾删
	SListPrint(plist);			//打印
	SListDestroy(&plist);		//销毁
	printf("\n");
}


void test2()
{
	SListNode* plist = NULL;				//一开始为空
	SListPushFront(&plist, 6);				//头插
	SListNode* pos = SListFind(&plist, 6);	//查找  
	//任意位置的向前插入
	SListInsert(&plist, pos, 10);			
	SListInsert(&plist, pos, 20);
	SListInsert(&plist, pos, 30);
	//任意位置的向后插入
	SListInsertAfter(pos, 40);
	SListInsertAfter(pos, 50);				
	SListPrint(plist);						//打印
	SListDestroy(&plist);					//销毁
	printf("\n");
}


void test3()
{
	SListNode* plist = NULL;
	//一开始为空
	SListPushBack(&plist, 1);	//尾插
	SListPushBack(&plist, 2);	//尾插
	SListPushFront(&plist, 3);	//头插
	SListPushFront(&plist, 4);	//头插
	SListPrint(plist);			//打印

	//修改任意pos处节点
	SListNode* pos = SListFind(&plist, 3);	//查找
	pos->data = 30;							//修改
	SListPrint(plist);						//打印
	SListDestroy(&plist);					//销毁
	printf("\n");
}

void test4()
{
		SListNode* plist = NULL;				//一开始为空
		SListPushBack(&plist, 1);				//尾插
		SListNode* pos = SListFind(&plist, 1);	//查找  
		//任意位置的向前插入
		SListInsert(&plist, pos, 10);
		SListInsert(&plist, pos, 20);
		SListInsert(&plist, pos, 70);
		//任意位置的向后插入
		SListInsertAfter(pos, 40);
		SListInsertAfter(pos, 50);
		SListInsertAfter(pos, 60);
		SListPrint(plist);						//打印
		SListEraseAfter(&plist,pos);			//删除pos节点位置后的数据
		SListEraseAfter(&plist,pos);			//删除pos节点位置后的数据
		SListEraseAfter(&plist,pos);			//删除pos节点位置后的数据
		SListPrint(plist);						//打印
		//修改任意pos处节点
		pos = SListFind(&plist, 70); 			//查找
		pos->data = 30;							//修改
		SListPrint(plist);						//打印
		SListDestroy(&plist);					//销毁
		printf("\n");
}
int main()
{
	test1();
	test2();
	test3();
	test4();
	return 0;
}

总结:

本文介绍了数据结构单链表的实现步骤,这一板块需要画图来理解,多画图,多调试才能学得明白,调试方面的话,可以参考我的001 VS配置c语言环境,以及一些入门技巧——“C”这一篇文章哦
ps:需要源代码的友友们,我已上传到Gitee 手撕无头单向非循环链表自取哦
后续会介绍带头双向循环链表,这种链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
在这里插入图片描述

posted @ 2023-04-24 12:35  Fan_558  阅读(10)  评论(0编辑  收藏  举报  来源