数据结构——链表


链表是一种物理结构上非连续,非顺序的存储结构。数据元素的顺序是通过链表中的指针链接起来的。

顺序表在插入数据时,可能会扩容,造成一定的消耗,如果新插入的数据量小,也会造成一定的空间浪费。链表可以很好的解决这两个问题。但链表也有其他不足。

一、链表的分类

根据单向或双向,带头或不带头,循环或不循环,链表可以分为八类。
其中头结点只保留链表第一个结点的地址,负责找到链表第一个结点,头结点不存储有效结点。

单向或双向
在这里插入图片描述
循环或者不循环
在这里插入图片描述
在物理结构上,链表是由一个个结点组成的,如图是零散的
在这里插入图片描述

在逻辑结构上,可以用一个个箭头表示可以找到下一个结点
在这里插入图片描述

其中pList就是头结点,0x12ff40是地址,存储的是下一个结点的地址,这个结点不存储有效数据。

如果是循环链表,最后一个结点存储的是第一个结点的地址。

二、常用单链表接口

这里的单链表指的是不带头不循环单向链表。

下面就开始实现一个链表。需要自定义一个结构体类型,成员变量由一个数据和一个指针构成。

typedef int linkDataType; //方便对数据的类型进行修改

typedef struct linkListNode
{
	linkDataType val;
	struct linkListNode* next;
}Node;

将结构体重命名为Node方便书写。

对于单链表需要实现的一些接口有这些。

//尾插
void LTpushback(Node** pphead, linkDataType x);
//头插
void LTpushfront(Node** pphead, linkDataType x);
//尾删
void LTpopback(Node** pphead);
//头删
void LTpopfront(Node** pphead);
//查找
Node* LTfind(Node* phead, linkDataType x);
//删除指定位置
void LTErase(Node** pphead, Node* pos);
//销毁
void LTDestroy(Node** pphead);
//打印,这个函数只是用来验证其他接口的正确性
void LTPrint(Node* phead);

开始一一实现。

1.尾插

对于尾插,第一步是要找到尾结点,然后链接上即可。由于实现的链表没有头结点,所以在链接的时候需要分结点数为0和不为0两种情况。

在结点数为0时:申请的结点可以看作是尾结点,所以可以直接返回,结束函数。
结点数不为0时:先找尾,在链接

代码如下

void LTpushback(Node** pphead, linkDataType x)
{
	assert(pphead);
	//创建新结点
	Node* newNode = (Node*)malloc(sizeof(Node));
	if (!newNode)
	{
		printf("malloc failed!");
		return;
	}
	newNode->val = x;
	newNode->next = NULL;
	//一个结点都没有
	if (!(*pphead))
	{
		(*pphead) = newNode;
		return;
	}
	//多于一个结点,先找尾结点
	Node* tail = (*pphead);
	while (tail->next)
	{
		tail = tail->next;
	}
	//链接到尾
	tail->next = newNode;
}

使用二级指针传参,是因为实参是指针。
尾结点的指针域为空,因此可以使用循环来找尾。链接就是将新结点的地址赋值给原来尾结点的指针域。

2.头插

同尾插,头插也分结点数位0和不为0

//头插
void LTpushfront(Node** pphead, linkDataType x)
{
	assert(pphead);
	//创建新结点
	Node* newNode = (Node*)malloc(sizeof(Node));
	if (!newNode)
	{
		printf("malloc failed!");
		return;
	}
	newNode->val = x;
	newNode->next = NULL;
	//没有结点
	if (!(*pphead))
	{
		(*pphead) = newNode;
		return;
	}
	//修改链接
	newNode->next = *pphead;
	*pphead = newNode;
}

3.尾删

尾删需要分两种情况,只有一个结点和多于一个结点。结点数如果为零,直接返回函数即可。

先找尾结点的前一个结点,在删除尾结点。删除后需要将前一个结点的指针域置空。

//尾删
void LTpopback(Node** pphead)
{
	assert(pphead);   //不能传一个空地址
	assert(*pphead);  //链表不可以是空链表
	//只有一个结点
	if (!(*pphead)->next)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//多于一个结点,先找尾
	Node* tail = (*pphead)->next;
	Node* cur = *pphead;
	while (tail->next)
	{
		cur = tail;
		tail = tail->next;
	}
	//删除结点
	free(tail);
	cur->next = NULL;
}

在这里插入图片描述
创建两个变量,cur指向第一个结点,tail指向后一个,当tail->next==NULL时,tail指向的结点就是尾结点,释放后,需要将前一个的指针域置空,防止野指针的发生。

4.头删

头删只需要保留下一个结点的地址,在删除第一个结点即可。

//头删
void LTpopfront(Node** pphead)
{
	assert(pphead);   //不能传一个空地址
	assert(*pphead);  //链表不可以是空链表
	//只有一个结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//有多个结点
	Node* front = (*pphead)->next;
	free(*pphead);
	*pphead = front;
}

5.查找

查找一个数据,循环遍历链表即可。

//查找
Node* LTfind(Node* phead, linkDataType x)
{
	assert(phead);
	Node* cur = phead;
	while (cur)
	{
		if (cur->val == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	printf("数据不存在\n");
	return NULL;
}

6.删除指定位置

删除头尾结点其实就是头删和尾删。删除指定位置时,需要不改变链表结构,即删除位置的前后结点需要链接起来,整个链表不能断。
在这里插入图片描述
创建一个遍历保存前一个结点的地址先改变链表的链接,在删除结点。不能先删除结点在改变链接,因为找不到下一个结点,即图中值为4的结点。

//指定位置删除
void LTErase(Node** pphead, Node* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	Node* prev = *pphead;
	//删除的结点是第一个
	if (prev == pos)
	{
		prev = pos->next;
		*pphead = prev;
		free(pos);
		return;
	}
	//找pos位置的前一个结点
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	//删除
	prev->next = pos->next;
	free(pos);
}

7.销毁和打印

这两个函数都是遍历一遍即可。销毁时需要注意保留下一个结点的地址。

//销毁
void LTDestroy(Node** pphead)
{
	assert(pphead);
	assert(*pphead);
	Node* cur = *pphead;
	while (cur)
	{
		Node* next = cur->next;
		free(cur);
		cur = next;
	}
}
//打印
void LTPrint(Node* phead)
{
	Node* cur = phead;
	while (cur)
	{
		printf("%d->", cur->val);
		cur = cur->next;
	}
	printf("NULL\n");
}

三、其他接口

//指定位置前插入
void LTInsert(Node** pphead, linkDataType x, Node* pos)
{
	assert(pphead);
	assert(*pphead);
	//创建新结点
	Node* newNode = (Node*)malloc(sizeof(Node));
	if (!newNode)
	{
		printf("malloc failed!");
		return;
	}
	newNode->val = x;
	Node* cur = *pphead;
	//如果pos指向第一个结点
	if (cur == pos)
	{
		newNode->next = pos;
		*pphead = newNode;
		return;
	}
	//找pos位置的前一个结点
	while (cur->next != pos)
	{
		cur = cur->next;
	}
	//插入
	newNode->next = pos;
	cur->next = newNode;
}
//在指定位置之后插入数据
void SLTInsertAfter(Node* pos, linkDataType x)
{
	assert(pos);
	//创建新结点
	Node* newNode = (Node*)malloc(sizeof(Node));
	if (!newNode)
	{
		printf("malloc failed!");
		return;
	}
	newNode->val = x;
	newNode->next = pos->next;
	pos->next = newNode;
}
//删除指定位置后的全部结点
void LTEraseAfter(Node** pphead, Node* pos)
{
	while (pos->next)
	{
		Node* next = pos->next;
		pos->next = next->next;
		free(next);
		next = NULL;
	}
}

单链表有部分缺陷,比如不能随机访问中间结点,找前一个结点时需要遍历,这些缺陷双向循环链表可以解决。

关于单链表就介绍到此了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值