基于C语言的单链表实现

Posted 胖仙人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于C语言的单链表实现相关的知识,希望对你有一定的参考价值。

->Gitee源代码点击这里<-
单链表是由不连续的空间组合而成的。每个空间称为单链表的一个结点。
一个结点中除了存放数据之外,还要存放下一个结点的地址,如果没有下一个结点,则存放空指针。表示其后面没有结点了

从逻辑上来看,就好像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:

此时链表为空。
在链表的头部插入第一个结点A,此时plist的值为A的地址值,plist指向第一个结点:

此时在链表头部再插入一个结点B,此时plist内存放的地址值就要变成结点B的地址:

并且此时需要注意,新插入的结点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直接指向插入的新结点

②链表内有数据时,我们需要先找到插入之前链表的尾(指向的下一个结点为空的即为尾),让原来的尾指向插入的结点。
图中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结点)

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;

特殊情况:当链表只有一个结点时,使用prev指针会出现空指针引用的情况,因此该情况需要单独拿出来考虑

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;

以上是关于基于C语言的单链表实现的主要内容,如果未能解决你的问题,请参考以下文章

基于C语言的单链表实现

数据结构 EP2 链表 C语言实现单链表及其增删查改

基于单链表实现一个非阻塞的栈

基于C语言的队列实现

基于C语言的队列实现

基于C语言的队列实现