基于C语言的单链表实现

本文详细介绍了如何使用C语言实现单链表,包括创建节点、头部插入、尾部插入、删除操作、查找、在指定节点后插入、删除指定节点后的节点以及修改节点值等操作,同时讨论了特殊情况下如链表只有一个节点时的处理策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

->Gitee源代码点击这里<-
单链表是由不连续的空间组合而成的。每个空间称为单链表的一个结点。
一个结点中除了存放数据之外,还要存放下一个结点的地址,如果没有下一个结点,则存放空指针。表示其后面没有结点了
image.png
从逻辑上来看,就好像A和B链接了起来。
我们通过结构体来定义链表的结点信息

typedef int LDataType;
typedef struct LinkedList
{
	LDataType data;        //数据值
	struct LinkedList* next;//下一个结点的地址
}LinkedList;

单链表的好处是按需开辟空间,有数据要插入时,就开辟一个结点,删除数据时,就释放一个结点。
所以在处理数据之前,我们需要先写出一个开辟结点的接口
由于我们不知道一个

//开辟新结点
LinkedList* BuyNewNode(LDataType data);
LinkedList* BuyNewNode(LDataType data)
{
	LinkedList* node = (LinkedList*)malloc(sizeof(LinkedList));
	if (node == NULL)
	{
		printf("Buy new node failed!\n");
		exit(-1);
	}
	node->data = data;
	node->next = NULL;   //由于在创建新节点时,我们不知道下一个结点的情况,所以默认存放空指针
	return node;
}

想要实现数据处理,我们还需要借助一个指针plist。指针类型和结构体类型相同。通过这个指针我们可以判断链表的情况。
当plist为空时,即plist不指向任何一个结点,则链表为空
当plist不为空时,即plist指向某个结点,则链表不为空。
plist初始化的值默认为NULL;

struct LinkedList* phead == NULL;

数据处理接口的设计:
一、头部插入数据
我们创建了plist,plist即代表链表。通过plist的状态来判断链表的状态。
plist的初始值为NULL:
image.png
此时链表为空。
在链表的头部插入第一个结点A,此时plist的值为A的地址值,plist指向第一个结点:
image.png
此时在链表头部再插入一个结点B,此时plist内存放的地址值就要变成结点B的地址:
image.png
并且此时需要注意,新插入的结点B中的指针部分存放的不再是空指针了,而是存放结点A的地址,以将两个结点关联起来

在这个过程中我们发现:
链表在数据处理时,plist的值是要发生变化的;所以接口在接收参数时,接受的应该是plist的地址(即传址调用)

void LinkedListPushHead(LinkedList** pplist, LDataType data);
void LinkedListPushHead(LinkedList** pplist, LDataType data)
{
	LinkedList* node = BuyNewNode(data);
	node->next = *pplist;
	*pplist = node;
}

二、尾部插入数据
尾部插入数据分需要分两种情况讨论:
①链表内没有数据时,plist直接指向插入的新结点
image.png
②链表内有数据时,我们需要先找到插入之前链表的尾(指向的下一个结点为空的即为尾),让原来的尾指向插入的结点。
图中A结点即为插入新结点之前的尾,尾插入结点B之后,B成为新的尾,A指向B;
![image.png](https://img-blog.csdnimg.cn/img_convert/fd782107917a8807883cdebae64f8ff4.png#clientId=udc03f381-13bd-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=159&id=ue7802d1f&margin=[object Object]&name=image.png&originHeight=212&originWidth=1009&originalType=binary&ratio=1&rotation=0&showTitle=false&size=15178&status=done&style=shadow&taskId=ua0b0e98c-6d02-4ec9-9802-12d31918253&title=&width=757)

void LinkedListPushBack(LinkedList** pplist, LDataType data);
void LinkedListPushBack(LinkedList** pplist, LDataType data)
{
	LinkedList* node = BuyNewNode(data);
	LinkedList* tail = *pplist;
	if (tail == NULL)
	{
		*pplist = node;
	}
	else
	{
		while (tail->next != NULL)   //通过遍历找到原来的尾
		{
			tail = tail->next;
		}
		tail->next = node;
	}
}

为了方便测试,还需要写一个打印链表的接口

void LinkedListPrint(LinkedList** pplist);
void LinkedListPrint(LinkedList** pplist)
{
	LinkedList* cur = *pplist;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

三、头删数据
头删示意图如下,删除头结点A,则A结点的空间需要被释放,plist不在指向A,而是指向A指向的下一个结点(即B结点)
image.png

void LinkedListPopHead(LinkedList** pplist);
void LinkedListPopHead(LinkedList** pplist)
{
	assert(*pplist != NULL);
	LinkedList* head = *pplist;
	*pplist = head->next;
	free(head);
	head = NULL;
}

四、尾删数据
尾删结点,首先需要找到尾,并释放尾结点,让尾结点之前的结点指向空
尾删示意图如下:结点A原本指向结点B,B指向NULL,尾删B之后,B空间被释放,结点A指向NULL
由于链表只能按着顺序往下走,所以当我们只创建一个tail指针找尾时,我们没办法访问尾指针之前的指针,此时则需要第二个指针prev来帮助记录tail指针前一个指针的位置,prev的初始值设为NULL;
image.png
特殊情况:当链表只有一个结点时,使用prev指针会出现空指针引用的情况,因此该情况需要单独拿出来考虑
image.png

void LinkedListPopBack(LinkedList** pplist);
void LinkedListPopBack(LinkedList** pplist)
{
	assert(*pplist != NULL);
	LinkedList* tail = *pplist;
	LinkedList* prev = NULL;
	if (tail->next == NULL)    //只有一个结点的情况
	{
		free(tail);
		tail = NULL;
		*pplist = NULL;
	}
	else
	{
		while (tail->next != NULL)   //有多个节点时,通过遍历找尾
		{
			prev = tail;             //tail要往后走,prev则记录tail之前的位置
			tail = tail->next;       //tail往后遍历
		}
		prev->next = NULL;
		free(tail);
		tail = NULL;
	}
}

五、查找
查找数据,查找到则返回结点的地址,否则返回NULL

LinkedList* LinkedListSearch(LinkedList** pplist, LDataType data);
LinkedList* LinkedListSearch(LinkedList** pplist, LDataType data)
{
	LinkedList* cur = *pplist;
	while (cur != NULL)
	{
		if (cur->data == data)
			return cur;
        else
			cur = cur->next;
	}
	return NULL;
}

六、在指定结点之后插入新结点
由于链表遍历的不可逆,使得访问某个结点之前的结点很不方便,因为接口设计时,设计为在指定结点之后插入新结点
此外,链表不能像顺序表一样利用下标随机访问,所以该接口的使用还需要配合查找接口。需要先查找指定的结点,获得该结点的地址,再将该地址作为pos传给插入接口

void LinkedListPushAfter(LinkedList* pos, LDataType data);
void LinkedListPushAfter(LinkedList* pos, LDataType data)
{
    assert(pos);
	LinkedList* node = BuyNewNode(data);
	node->next = pos->next;
	pos->next = node;
}

七、删除指定结点之后的一个结点
接口设计成删除指定位置结点之后的结点,原因同上。使用时同样也需要配合查找接口的调用
需要注意的是,pos不能为尾结点,需要加断言判断

void LinkedListPopAfter(LinkedList* pos, LDataType data);
void LinkedListPopAfter(LinkedList* pos, LDataType data)
{
	assert(pos);
    assert(pos->next != NULL);
	LinkedList* next = pos->next;
	pos->next = next->next;
	free(next);
	next = NULL;
}

八、修改结点值
该接口也需要配合查找接口的使用

void LinkedListModify(LinkedList* pos, LDataType data);
void LinkedListModify(LinkedList* pos, LDataType data)
{
	assert(pos);
	pos->data = data;
}

九、链表的销毁

void LinkedListDestroy(LinkedList** pplist);
void LinkedListDestroy(LinkedList** pplist)
{
	LinkedList* cur = *pplist;
	while (cur != NULL)
	{
		LinkedList* next = cur->next;
		free(cur);
		cur = next;
	}
	*pplist = NULL;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值