链表详解—单链表与双向链表

一.单链表

1.链表的概念及结构

(1)概念:

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

(2)结构:

 与顺序表不同的是,链表里的每个节点都是独立申请的空间。节点的组成主要有两个部分:当前节点要保存的数据和保存下一个节点的地址(指针变量)。

图中指针变量plist保存的是第一个节点的地址,即plist“指向”第一个节点,如果我们希望plist指向第二个节点时,只需要修改plist保存的内容为0x0012FFA0。

为什么还需要指针变量来保存下一个节点的位置?

链表中每个节点都是独立申请的(即需要插入数据时才去申请一块节点的空间),我们需要通过指针变量来保存下一个节点的位置才能从当前节点找到下一个节点。

(3)每个节点对应的结构体代码

(假设当前保存的节点为整数)


//定义节点的结构
typedef struct SListNode
{
	int date;//节点数据
	struct SListNode* next;//指向下一个节点的指针
}SLTNode;

 (4)链表的打印

给定的链表结构中,如何实现链表节点从头到尾的打印?

 (5)思考

当我们想保存的数据类型为字符型,浮点型或者其他自定义类型时,该如何修改?

补充说明:

•  链表机构在逻辑上是连续的,在物理结构上不一定连续

•  节点一般是从堆上申请的

•  从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续

 我们可以用到重命名typedef,用STDateType代替链表要保存的数据类型。当我们想保存的数据类型为字符型,浮点型或者其他自定义类型时,将int修改成我们想要的数据类型。

typedef int SLTDataType;

2.单链表的实现(无头+单向+非循环链表

SList.h

typedef int SLTDataType;
//定义节点的结构
typedef struct SListNode
{
	SLTDataType date;
	struct SListNode* next;
}SLTNode;


void SLTPrint(SLTNode* phead);

//头部插入删除/尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);

SList.c 

(1)链表的打印

//链表的打印
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->date);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

(2)创建新节点的函数

//创建新节点的函数
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);//异常退出(非零),正常退出是(0)
	}
	newnode->date = x;
	newnode->next = NULL;
	return newnode;
}

(3)尾插

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);//但*pphead可以为空,表示该节点的地址为空
	SLTNode* newnode = SLTBuyNode(x);
	//空链表和非控链表
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* ptail = *pphead;
		//找尾
		while (ptail->next)
		{
			ptail = ptail->next;
		}//ptail指向的就是最后一个节点
		ptail->next = newnode;
	}
}

(4)头插

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;//使newnode成为新的头结点!
}

(5)尾删

//尾删
void SLTPopBack(SLTNode** pphead)
{
	//*pphead 也不能为空(链表不能为空),不然就没有删除的节点
	assert(pphead && *pphead);
	//链表只有一个节点的情况
	if ((*pphead)->next == NULL)//->的优先级高于*
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* ptail = *pphead;
		SLTNode* prev = *pphead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}//ptail 为尾节点
		//prev为尾节点的前一个节点
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}

(6)头删

//头删
void SLTPopFront(SLTNode** pphead)
{
	//*pphead 也不能为空(链表不能为空),不然就没有删除的节点
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

(7)查找链表中有没有值为x的节点,如果有的话返回该节点

//查找链表中有没有值为x的节点,如果有的话返回该节点
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* ptail = phead;
	while (ptail)
	{
		if (ptail->date == x)
		{
			return ptail;
		}
		ptail = ptail->next;
	}
	//没有找到
	return NULL;
}

(8)在指定位置之前插入数据

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);//链表不能为空,不然就找不到节点pos
	assert(pos);
	//链表只有一个节点
	if (pos == *pphead)//说明是头插
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = SLTBuyNode(x);
		SLTNode* ptail = *pphead;
		while (ptail->next != pos)
		{
			ptail = ptail->next;
		}//ptail为pos节点的前一个节点
		ptail->next = newnode;
		newnode->next = pos;
	}
}

(9)删除pos节点

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && pos);
	//pos为头结点——>头删
	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}
	else//po不是头结点
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}//prev 为pos 的前一个节点
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

(10)在指定位置之后插入数据

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

(11)删除pos之后的节点

//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert((pos->next) && pos);
	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
	next = NULL;
}

(12)销毁链表—>销毁一个一个的节点

//销毁链表——>销毁一个一个的节点
void SListDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;//别忘记销毁头结点
}

3.链表的分类

链表结构非常多样,以下情况组合起来就有8种。

(1)单项或双向

(2)带头或不带头

(3)循环或不循环

虽然有那么多的链表结构,但我们最常用的只有两种:

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

•  带头双向循环链表:结构最复杂,一般用在单独存放数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码1实现以后会发现结构会带来很多优势。

二.双向链表

1.双向链表的结构

 注意:这里的“带头”跟前面说的“头结点”是两个概念,实际前面的在单链表讲解中称呼不严谨。

带头链表里的头结点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是“放哨”作用。

“哨兵位”存在的意义:
遍历循环链表避免死循环。

2.双向链表的实现

typedef int LTDateType;
typedef struct ListNode
{
	LTDateType date;
	struct ListNode* next;
	struct ListNode* prev;

}LTNode;

//声明双向链表中提供的方法


//双向链表初始化
LTNode* LTInit2();//1以返回值的形式初始化,为了保持接口一致性
//void LTInit(LTNode** pphead);//2

//打印双向链表
void LTPrint(LTNode* phead);


//插入数据之前,链表必须初始化到只有一个节点的情况
//不能改变哨兵位的地址,因此传一级即可
//尾插
void LTPushBack(LTNode* phead, LTDateType x);
//头插
void LTPushFront(LTNode* phead, LTDateType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDateType x);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDateType x);
//删除pos节点
void LTErase(LTNode* pos);//不传二级指针原因:保持接口一致性
//双向链表的销毁
void LTDestory(LTNode* phead);


//LTErase和LTDestory参数理论上要传二级指针,因为我们需要让形参的
//改变影响实参,但是为了保持接口一致性才传的一级。
//传一级指针的问题在于,当形参phead置为NULL后,实参plist不会被修改
//为NULL,因此解决方法是:调用完方法后手动将实参置为NULL。

//plist(LTDestory的方法调用后,plist保存的空间已经被释放掉了)
//若后续还需要用plist指针就会有问题。

(1)打印双向链表

//打印双向链表
void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->date);
		pcur = pcur->next;
	}
	printf("\n");
}

(2)创建一个节点

//创建一个节点
LTNode* LTBuyNode(int x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	node->date = x;
	node->next = node->prev = node;
}

(3)双向链表的初始化

//双向链表的初始化
//1.以返回值的形式初始化
LTNode* LTInit2()
{
	LTNode* pcur = LTBuyNode(-1);
	return pcur;
}
//2.以参数形式初始化
//void LTInit(LTNode** pphead)
//{
//	//给双向链表创建一个哨兵位
//	*pphead = LTBuyNode(-1);
//}

(4)尾插

//尾插(将新节点插到头节点(哨兵位)之前)
void LTPushBack(LTNode* phead, LTDateType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//将phead  phead->prev  newnode  正确排列
	//先用新节点
	newnode->prev = phead->prev;
	newnode->next = phead;
	//下面两行代码不能位置改变
	phead->prev->next = newnode;
	phead->prev = newnode;
}

(5)头插

//头插(在链表里第一个有效数字的前面插)(往头结点后面插)
void LTPushFront(LTNode* phead, LTDateType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->prev = phead;
	newnode->next = phead->next;

	phead->next->prev = newnode;
	phead->next = newnode;
	//phead->next = newnode;
	//newnode->next->prev = newnode;(这两行也可以)
}

(6)尾删

//尾删
void LTPopBack(LTNode* phead)
{
	//链表必须有效且不能为空(只有一个哨兵位的情况)
	assert(phead && (phead->next) != phead);
	phead->prev = phead->prev->prev;
	phead->prev->next = phead;
	//或
	//LTNode* del = phead->prev;
	//del->prev->next = phead;
	//phead->prev = del->prev;
	删除del节点
	//free(del);
	//del == NULL;
}

(7)头删

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead && (phead->next) != phead);
	phead->next = phead->next->next;
	phead->next->prev = phead;
}

(8)查找

//查找
LTNode* LTFind(LTNode* phead, LTDateType x)
{
	LTNode* pcur = phead->next;
	while(pcur != phead)
	{
		if (pcur->date == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}//没有找到
	return NULL;
}

(9)在pos位置之后插入数据

//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDateType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;
}

(10)删除pos节点

//删除pos节点
void LTErase(LTNode* pos)
{
	//理论上pos节点不能为phead,但是没有phead无法参加校验
	assert(pos);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

(11)双向链表的销毁

//双向链表的销毁
void LTDestory(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//此时pcur指向phead,phead没有被销毁
	free(phead);
	phead = NULL;
}

 三.顺序表与链表的优缺点分析

备注:缓存利用率参考存储体系结构及局部原理性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/612371.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

02c++模板部分讲解

1理解函数模板 #include<iostream> using namespace std;//函数模板 template<typename T> //定义一个模板参数列表 //模板类型参数 typename/class bool compare(T a, T b) {cout << "template compare: " << endl;return a > b; }temp…

centos7.9系统rabbitmq3.8.5升级为3.8.35版本

说明 本文仅适用rabbitmq为RPM安装方式。 升级准备 查看环境当前版本&#xff1a; # cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) # rabbitmqctl status Status of node rabbitmq01 ... RuntimeOS PID: 19333 OS: Linux Uptime (seconds): 58 Is under …

Blazor入门-基础知识+vs2022自带例程的理解

参考&#xff1a; Blazor 教程 - 生成首个应用 https://dotnet.microsoft.com/zh-cn/learn/aspnet/blazor-tutorial/intro Blazor基础知识&#xff1a;Visual Studio 2022 中的Blazor开发入门_vs2022 blazor webassembly-CSDN博客 https://blog.csdn.net/mzl87/article/detail…

【Unity自制手册】unity常用API大全——一篇文章足以(十万字详解)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 收录于专栏 unity 实战系列 ⭐相关文章⭐ ⭐【软件设计师高频考点暴击】 -本站最全-unity常用API大全&#xff08;万字…

性能测试 --概念

什么是性能测试 性能测试和功能测试都是在系统测试阶段运行, 两者有什么区别呢? 案例:豌豆射手和三线射手都是射手, 它们的功能都是向前发射豌豆进行攻击, 能够攻击到地面的僵尸. 但是从性能上来讲, 豌豆射手只能攻击到一路的僵尸, 而三线射手能同时攻击三路(注:放在边路实际…

如何统计区域内部公路总长度和绘制数据直方图

交通对区域经济发展具有重要的意义&#xff0c;诸多经济学文献均对此进行了深入探讨&#xff0c;那么如何在ArcGIS上统计区域内部交通路线的总长度呢&#xff1f;本文以全国各省份主要公路总长度为例详细介绍了相关ArcGIS以及统计直方图的操作。 工具&#xff1a; 使用ArcGIS中…

【JavaScript】内置对象 - 数组对象 ③ ( 数组反转 - reverse 方法 | 数组排序 - sort 方法 | 自定义数组排序规则 )

文章目录 一、数组排序1、翻转数组元素 - reverse()2、数组元素排序 - sort() 默认从小到大排序3、数组元素排序 - sort() 自定义排序规则4、数组元素排序 - sort() 自定义降序排序简化写法 Array 数组对象参考文档 : https://developer.mozilla.org/zh-CN/docs/Web/JavaScript…

C#编程模式之享元模式

创作背景&#xff1a;各位朋友&#xff0c;我们继续学习C#的编程模式&#xff0c;本文主要介绍享元模式。享元模式是一种结构型设计模式&#xff0c;它主要用于减少创建对象的数量&#xff0c;从而提高程序性能。它通过共享对象的方式来减少内存的使用&#xff0c;特别是系统中…

彩虹聚合DNS管理系统

聚合DNS管理系统可以实现在一个网站内管理多个平台的域名解析&#xff0c;目前已支持的域名平台有&#xff1a;阿里云、腾讯云、华为云、西部数码、CloudFlare。本系统支持多用户&#xff0c;每个用户可分配不同的域名解析权限&#xff1b;支持API接口&#xff0c;支持获取域名…

同城组局同城活动找搭子小程序JAVA源码面芽组局的实现方案

功能概述 基于微信小程序开发的一款软件&#xff0c;支持用户动态发布、私信聊天关注、礼物充值打赏、发起活动组局、用户报名参与、支持商家入驻&#xff0c;对接广告功能等。 活动发布&#xff1a;用户可以在平台上发布各种类型的活动&#xff0c;如户外徒步、音乐会观赏、…

回炉重造java----单列集合(List,Set)

体系结构: 集合主要分为两种&#xff0c;单列集合collection和双列集合Map&#xff0c;区别在于单列集合一次插入一条数据&#xff0c;而双列的一次插入类似于key-value的形式 单列集合collection 注:红色的表示是接口&#xff0c;蓝色的是实现类 ①操作功能: 增加: add()&am…

探索震坤行API:一键解锁高效工业用品采购新纪元!

震坤行是一家专注于工业用品的B2B电商平台&#xff0c;为企业客户提供一站式的工业用品采购服务。虽然震坤行没有直接公开通用的API接口供开发者调用&#xff0c;但通常大型企业或合作伙伴之间可以通过API进行系统集成和数据交互。以下是一个假设性的震坤行API接口调用示例与代…

博特激光:355nm高精度紫外激光打标机带来极致工艺

紫外激光打标机在现代制造业和技术中的应用&#xff0c;的确在准确度和精密度方面带来了革命性的提高。特别是在微电子、半导体、医疗器械、高端消费品等需要高精度、高清晰打标的行业&#xff0c;紫外激光打标机以其独特的优势&#xff0c;赋予产品极致的工艺品质。 以下是UV激…

软件测试之 接口测试 Postman使用

接口测试 URL HTTP协议 HTTP 请求部分 HTTP响应部分 Postman使用 界面介绍 这里 注意 如果你无法访问 那么 captchaImage这个打错了&#xff0c;给的资料中是错误的地址 https://kdtx-test.itheima.net/api/captchaImage登录接口 科大天下 第一个接口的登录设置 https://kd…

基于vgg16和efficientnet卷积神经网络的天气识别系统(pytorch框架)全网首发【图像识别-天气分类】

一个能够从给定的环境图像中自动识别并分类天气&#xff08;如晴天、多云、雨天、雪天等&#xff09;的系统。 技术栈&#xff1a; 深度学习框架&#xff1a;PyTorch基础模型&#xff1a;VGG16与EfficientNet任务类型&#xff1a;计算机视觉中的图像分类 模型选择 VGG16 VGG…

Redis 源码安装(CentOS 单机)

序言 本文给大家介绍如何在 CentOS 上&#xff0c;通过 Redis 源码单机部署 Redis 服务。 一、部署流程 通过官网下载源码 # 下载源码 wget https://download.redis.io/redis-stable.tar.gz# 解压源码包 tar -xzvf redis-stable.tar.gz在 linux 中执行以下命令&#xff0c;安…

信息系统项目管理师0101:项目建议与立项申请(7项目立项管理—7.1项目建议与立项申请)

点击查看专栏目录 文章目录 第七章 项目立项管理7.1项目建议与立项申请1.立项申请概念2.项目建议书内容记忆要点总结第七章 项目立项管理 项目立项管理是对拟规划和实施的项目技术上的先进性、适用性,经济上的合理性、效益性,实施上的可能性、风险性以及社会价值的有效性、可…

Java面试——MyBatis

优质博文&#xff1a;IT-BLOG-CN 一、MyBatis 与 JDBC 的区别 【1】JDBC 是 Java 提供操作数据库的 API&#xff1b;MyBatis 是一个持久层 ORM 框架&#xff0c;底层是对 JDBC 的封装。 【2】使用 JDBC 需要连接数据库&#xff0c;注册驱动和数据库信息工作量大&#xff0c;每…

Html生成自定义函数的图形(2024/5/10)

大概效果如下&#xff1a; 可以自定义函数和x的定义域。 我们可以使用数学表达式解析库来解析用户输入的函数方程&#xff0c;并根据给定的 x 区间计算函数的值&#xff0c;然后使用图表库绘制图形。 在这里&#xff0c;我将使用 math.js 库来解析数学表达式&#xff0c;并使…

linux fdisk 银河麒麟操作系统 v10 磁盘分区和挂载 详细教程

1查看 未加载的磁盘 fdisk -l 2 开始分区 fdisk /dev/vdb #查看分区 #新建分区和保存 3 格式化和挂载 fdisk -l mkfs.xfs /dev/vdb1 #查看uuid blkid /dev/vdb1 mkdir /data vi /etc/fstab UUID209daa-fb1c-48f2-bf5e-e63f38cb8a /data xfs defaults 0 0 #加载下 mo…