【数据结构】单链表详解-无头单向不循环链表

1、前言

了解过顺序表的缺陷后,我们知道必要的优化在于:
1.按需申请释放;
2.不要挪动数据。
那么,链表又如何能做到真正的按需扩容?

2、链表简介

链表是一种物理存储上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
表现形式:单链表、双链表和循环链表
主要特征:单向、双向、带头、不带头、循环、不循环
链表的八种结构:带头单向循环链表、带头单向非循环链表、带头双向循环链表、带头双向非循环链表、无头单向循环链表、无头单向非循环链表、无头双向循环链表、无头双向非循环链表

2.1组成

链表:由若干个同一结构类型的结点依次串联而成,每个结点保存下一个结点的地址。
结点:数据域和指针域。(结点类型为结构体,方便存储不同类型的数据和指针)
数据域:存储数据元素。
指针域:存储下一个结点的地址。

2.2逻辑结构

每个结点分为两个区域:数据域和指针域。(一般逻辑结构是现象出来便于理解学习的)

在这里插入图片描述

2.3物理结构

反映内存中结构的存储以及结点之间的关系。
在这里插入图片描述
与顺序表不同的是,内存地址完全是随机的,不一定相邻。

3、创建结构体

//创建单链表的结构体(即结点类型,包含组成链表的数据和指针)
typedef int DataType;

typedef struct ListNode
{
	DataType data;//数据域:存储数据
	struct ListNode* next;//指针域:存储下一个结点的地址
}LTNode;

4、链表接口

//接口
//创建结点
LTNode* BuyLTNode(DataType x);
//创建单链表(可以不写)
LTNode* CreatList(int n);
//打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode** pphead, DataType x);//找尾
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPushFront(LTNode** pphead, DataType x);
//头删
void LTPopFront(LTNode** pphead);
//查找
LTNode* LTFind(LTNode* phead, DataType x);
//先find后insert/erase
//在pos结点之后插入数据
void LTInsertAfter(LTNode* pos, DataType x);
//pos结点之前插入数据
void LTInsert(LTNode** pphead, LTNode* pos, DataType x);
//删除pos结点之后的一个数据
void LTEraseAfter(LTNode* pos);
//删除pos结点位置数据
void LTErase(LTNode** pphead, LTNode* pos);
//销毁
void LTDestroy(LTNode** pphead);

为什么部分接口函数在实现时需要传递二级指针参数?

//测试
void test()
{
	DataType* plist = NULL;
}

这是因为实参作为一个一级指针指向内存空间地址,如果想对头结点进行操作,就需要使用二级指针将改变的数据传递回实参所在的函数内。另外,二级指针用在这个地方可以作为最优解(C语言)看待,摒弃了许多麻烦的操作。

5、创建结点

//创建结点
//由于链表在内存中的物理地址是随机的,所以创建结点时无需使用扩容
LTNode* BuyLTNode(DataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = x;    //确定数据
	newnode->next = NULL; //确定下一个结点的地址指针
	//创建新结点时置空,方便后续操作

	return newnode;
}

6、创建单链表

//创建单链表
LTNode* CreatList(int n)
{
	LTNode* phead = NULL;
	LTNode* ptail = NULL;
	int x = 0;
	for (int i = 0; i < n; i++)
	{
		scanf("%d", &x);
		LTNode* newnode = BuyLTNode(x);
		if (phead == NULL)
		{
			ptail = phead = newnode;
		}
		else
		{
			ptail->next = newnode;
			ptail = newnode;
		}
	}
	//ptail->next = NULL;
	return phead; //返回phead避免丢失
}

为什么链表的物理结构中地址是随机的,不一定连续的?
因为结点在申请空间时是根据循环一个一个申请的,所以这些结点在内存中分配的空间并不是连续的,而是随机分配的空间,所以链表并不能像顺序表一样可以随机下标访问元素。

7、打印单链表

//打印单链表
void LTPrint(LTNode* phead)
{
	LTNode* cur = phead;
	while (cur != NULL)
	{
		//printf("%d->", cur->data);
		printf("[%d|%p]->", cur->data, cur->next);
		cur = cur->next;
	}
	printf("NULL\n");
}

打印单链表时,从头指针位置开始依次向后打印,当(打印)指针指向NULL时结束。

8、增加结点

8.1单链表的头插

//头插
//单链表的头插插入方便
void LTPushFront(LTNode** pphead, DataType x)
{
	LTNode* newnode = BuyLTNode(x);	//申请新节点
	//先让新结点记住头结点的位置,然后将头指针指向新结点
	newnode->next = *pphead;		
	*pphead = newnode;
}

头插结点:首先申请新结点,然后将新结点的指针域指向头结点,最后让头指针指向新结点即可。

8.2单链表的尾插

//尾插
void LTPushBack(LTNode** pphead, DataType x)//找尾
{
	LTNode* newnode = BuyLTNode(x); //申请新结点
	if (*pphead == NULL) //判断头结点
	{
		*pphead = newnode;
	}
	else
	{
		LTNode* tail = *pphead;
		while (tail->next) //利用结点存储的地址寻找结束条件
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

尾插结点:核心任务就是找尾,注意头结点为空需要另外判断。

8.3单链表指定位置插入结点

//指定位置之后插入结点
void LTInsertAfter(LTNode* pos, DataType x)
{
	assert(pos);
	LTNode* newnode = BuyLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
//指定位置之前插入结点
void LTInsert(LTNode** pphead, LTNode* pos, DataType x)
{
	assert(pos);
	if (*pphead == pos)
	{
		LTPushFront(pphead, x);
	}
	else
	{
		LTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		LTNode* newnode = BuyLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

9、删除结点

9.1单链表的头删

//头删
void LTPopFront(LTNode** pphead)
{
	assert(*pphead);
	LTNode* tail = *pphead; //记录第一个节点的位置
	*pphead = tail->next;   //头节点存入下一个节点
	free(tail);				//释放临时空间
	tail = NULL;			//记录节点置空
}	

如果是空表,可以无需做处理;如果不是空表,可以直接让头指针指向第二个结点,然后释放第一个结点的内存空间。

//头删
void SListPopBack(SLTNode** phead)
{
    assert(*phead);
    //*和->优先级相同,使用时需注意括号应用
	SLTNode* next = (*phead)->next; //找到第二个节点
	free(*phead);					//释放头节点
	*phead = next;					//将第二个节点改为头节点
}

9.2单链表的尾删

//尾删1
void LTPopBack(LTNode** pphead)
{
	assert(*pphead);
	//双指针操作
	LTNode* prev = *pphead;
	LTNode* tail = *pphead;
	//tail-最后一个节点
	//prev-倒数第二个节点
	while (tail->next)	
	{
		prev = tail;
		tail = tail->next;
	}
	if (prev == tail) //特殊情况,链表只有一个节点
	{
		*pphead = NULL; //头指针置空
		free(prev);		//释放prev
	}
	else
	{
		prev->next = NULL;//置空倒数第二个节点的指针域
		free(tail);       //释放最后一个节点
		tail = NULL;	  //及时置空
	}
}

尾删需要注意不同情况下的单链表删除不同:

  1. 单链表为空;
  2. 单链表只有一个元素;
  3. 单链表有多个元素。
//尾删2
void LTPopBack(LTNode** pphead)
{
	assert(*pphead);//列表为空不能删除
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		LTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

9.3单链表指定位置删除结点

//删除指定位置之后的结点
void LTEraseAfter(LTNode* pos)
{
	assert(pos);
	if (pos->next == NULL) //判断指定位置是否为最后一个结点的地址
	{
		return;
	}
	else
	{
		LTNode* cur = pos->next; //待删除结点(需要保留方便后续的释放)
		pos->next = cur->next;	  //pos结点指向待删除结点的下一个结点,完成单链表的删除
		free(cur);				  //释放临时变量
	}
}
//删除指定位置的结点
void LTErase(LTNode** pphead, LTNode* pos)
{
	assert(pos);
	assert(*pphead);

	if (pos == *pphead)	//特殊情况,删除头结点
	{
		LTPopFront(pphead); //复用头删
	}
	else
	{
		LTNode* prev = *pphead;	//使用头指针
		while (prev->next != pos)	//找到待删除结点的前一个节点地址
		{
			prev = prev->next;
		}
		prev->next = pos->next;	//prev指向pos下一个节点位置
		free(pos);	//释放pos
	}
}

10、查找

//查找结点(根据数据)
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* cur = phead;
	//遍历单链表(根据值查找)
	while (cur)
	{
		if (cur->data == x) 
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

捌音

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值