【数据结构】线性表----链表详解

数据结构—-链表详解

目录

文章目录

    • 链表的定义
    • 链表的构成
    • 链表的分类
      • 双向和单向
      • 带头和不带头
      • 循环和不循环
    • 链表的命名
    • 基本操作的实现
      • 初始化
      • 打印
      • 取值
      • 查找
      • 插入
      • 指定位置插入
      • 删除
      • 删除
      • 销毁
    • 部分其他链表的代码实现
      • 循环链表
      • 双向链表
    • 优点/缺点(对比顺序表)
      • 优点
      • 缺点

链表的定义

前面我们介绍的顺序表,在逻辑结构和物理结构上都是线性、连续的关系,那么我们在篇尾的时候也提到了是否有一种顺序表,无需在物理结构上连续,从而达到更好存取的目的呢?今天它来了。

链表(LinkList)属于线性表的一种,以下是百度百科关于链表的定义:

在这里插入图片描述

总结下来,我们可以看出:

在结构上链表并不是像顺序表那样底层结构是数组,而是包含两个区域:数据域指针域。我们可以类比成火车,火车每一节车厢实际上是独立的,也就好比数据域,存储着各自的数据;但每一节车厢之间都会由一条链连接在一起,从而形成整个火车,链也就好比指针域,起到连接该车厢和指向下一车厢的作用。而实际上这样的存储结构也就是为什么链表的物理结构可以不连续,而逻辑结构依旧是连续的

这样的好处就是当我们需要对指定元素进行移动、替换、存取等操作的时候,我们可以一步到位,无需挪动数据,无需扩容,不会造成空间上的浪费——好比你火车更换车厢,总不可能让整个火车都为你而改动吧。

在这里插入图片描述

所以,如果要用一句话概括链表是什么,我们可以说:链表是一种线性数据结构,由结点组成,每个结点包含一个数据元素也就是数据域和指向下一个节点的指针也就是指针域。(叫法:结点或者节点都行)

在这里插入图片描述

联想:指针域的作用

实际上,在后续的数据结构中,还有很多类型的数据结构会使用到指针域这个概念,或者可以说是结点的概念。结点指地是组成数据元素的存储映像,往往指针就是指向结点的,鉴于它不受物理空间限制的优点,往往都会使用指针和结点来更高效率地实现数据结构。

链表的构成

数据域(data):存放实际数据

指针域(next):存放下一节点的首地址

结点:由数据域和指针域两部分信息组成的数据元素的存储映像

链表的分类

我们平常使用的最多的就是单链表——不带头单向不循环链表

既然有不带头,那么必然就有带头;既然有单向,那么必然就有双向;既然有不循环,那么必然就有循环

接下来针对这三个方向进行介绍。

双向和单向

单向链表:每个节点包含一个指向下一个节点的指针。单向链表只能从头节点开始遍历,无法从尾节点向前遍历。
双向链表:每个节点包含一个指向下一个节点和一个指向前一个节点的指针。双向链表可以从头节点或尾节点开始遍历,可以方便地在链表中间插入或删除节点。(向的描述与指针域的描述是一致的,单向只有一个指针域,而双向有两个)

可以通俗地理解为,单向为单行道,只能从头走到尾;双向为双行道,既可退又可进。

带头和不带头

带头链表:指第一个结点叫做头结点,不存储有效数据,只作为指引结点
不带头链表:指第一个结点不叫做头结点,就是第一个结点,存储有效数据

注意这里指的带头就是带有头结点的意思,头结点不存储任何数据,它仅仅用于指向第一个结点。优点是操作链表时统一处理,不需要特殊处理第一个节点,代码更加简洁清晰。缺点是需要额外的头节点,占用额外的空间。

循环和不循环

循环链表:最后一个节点的指针指向第一个节点,形成一个环状结构。循环链表可以从任意节点开始遍历,但需要注意避免出现死循环的情况。
不循环链表:最后一个节点的指针直接指向空,仅仅作为线性结构。

虽然链表种类之多,例如还有带有尾结点的链表等等,但在实际应用中只有两种:**单链表(不带头单向不循环链表)双向链表(带头双向循环链表)**使用得最多,因为它们两个的特性加起来即为所有特性,而其他类型的链表也就不难创建了。

链表的命名

typedef int SLTDataType;
typedef struct SListNode
{
     SLTDataType data;
     struct SListNode* next; 
}SLTNode

单链表

SLTDataType、ElemType、LinklistType等等(根据自己的偏好设定),以下都命名为SLTDataType

数据域

SLTDataType data; //保存结点数据

指针域(*

struct SListNode* next;//保存下一结点地址

操作

SLTPushBack//针对操作的命名一般在操作前缀添加SLT等代表其为链表的大写字母缩写

基本操作的实现

有关链表的操作,下方是一些举例


//链表的头插、尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);

//链表的头删、尾删
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);

//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x);

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);

//销毁链表
void SListDesTroy(SLTNode** pphead);
//打印
void SLTPrint(SLTNode* phead);

接下来针对上述操作进行详细介绍。

初始化

即构造一个空链表

void SlistTest01() {
	//一般不会这样去创建链表,这里只是为了给大家展示链表的打印
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;
	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;
	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;
	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;
//当我们定义好了数据域之后,指针域应该如何定义呢?
    node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;
//像这样,让指针域指向该节点的下一节点,达到链接的目的
SLTNode* SLTBuyNode(SLTDataType x) {
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {//如果开辟失败的情况
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;//开辟数据域
	newnode->next = NULL;//开辟指针域

	return newnode;
}

打印

在接下来承担显示的作用,将链表的结构可视化

void SLTPrint(SLTNode* phead) {
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;//使用next指针指向下一节点
	}
	printf("NULL\n");
}

取值

与顺序表的取值不一样,因为链表物理结构是不连续的,所以在链表中进行访问的时候不能直接随机访问,只能从首元素出发遍历进行访问。

Status SLTGet(LinkList L, int i, ElemType& e)//获取线性表L中的耨个数据元素的内容,通过变量e返回
{ 
	p = L->next;                    //初始化,p指向首元结点,
	j = 1;                          //初始化,j为计数器
	while (p&&j < i)                //向后扫描,直到p指向的第i个元素或p为空
	{
		p = p->next;                //p指向下一个结点
		++j;
	}
	if (!p || j > i)                //第i个元素不存在,抛出异常
		return NULL;   
	e = p->data;                    //取第i个元素
	return OK;
}

查找

查找操作同顺序表类似,都是哦才能够首元结点开始,依次将元素与给定值进行比较。

//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x) {
	assert(pphead);

	//遍历链表
	SLTNode* pcur = *pphead;
	while (pcur) //等价于pcur != NULL
	{
		if (pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}
	//没有找到
	return NULL;
}

插入

插入分为尾插头插指定位置插入,而指定位置插入又分为指定位置之前和之后

尾插

//尾插
void SLTPushBack(SLTNode** pphead/*为什么是**二级指针?因为*pphead这个指针在传参时实际上还是传值而不是传地址,需要传这个指针的地址也就是使用二级指针*/, SLTDataType x) {
	assert(pphead);

	SLTNode* newnode = SLTBuyNode(x);

	//链表为空,新节点作为phead
	if (*pphead == NULL) {
		*pphead = newnode;
		return;
	}
	//链表不为空,找尾节点
	SLTNode* ptail = *pphead;
	while (ptail->next)//并非ptial不能为空,而是ptail->next不能指向空
	{
		ptail = ptail->next;
	}
	//ptail就是尾节点
	ptail->next = newnode;
}

头插

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);

	//newnode *pphead
	newnode->next = *pphead;
	*pphead = newnode;
}

指定位置插入

之前

//在指定位置之前插入数据
//关键:找到前驱节点
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
	assert(pphead);
	assert(pos);
	//要加上链表不能为空,如果链表都空了,必然找不到前驱节点
	assert(*pphead);

	SLTNode* newnode = SLTBuyNode(x);
	//pos刚好是头结点
	if (pos == *pphead) {
		//头插
		SLTPushFront(pphead, x);
		return;
	}

	//pos不是头结点的情况(如果不分情况讨论,那么prev就永远找不到pos)
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	//prev -> newnode -> pos
	prev->next = newnode;
	newnode->next = pos;
}

之后

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
	assert(pos);

	SLTNode* newnode = SLTBuyNode(x);
    //错误操作:(顺序错误,导致pos->next不再指向下一节点
	//pos->next=newnode 
    //newnode->next=pos->next
	
    //正确操作:
    newnode->next = pos->next;
	pos->next = newnode;
}

删除

删除又分为尾删头删指定结点删除以及指定位置后续结点删除

尾删

//尾删
void SLTPopBack(SLTNode** pphead) {
	assert(pphead);//第一个节点不能为空
	//链表不能为空
	assert(*pphead);

	//链表不为空

	if ((*pphead)->next == NULL)//如果只有一个节点 
    {
		free(*pphead);//直接释放
		*pphead = NULL;//置空
		return;
	}
    //如果有多个节点
	SLTNode* ptail = *pphead;
	SLTNode* prev = NULL;
	while (ptail->next)//不能指向空
	{
		prev = ptail;
		ptail = ptail->next;
	}
	
	prev->next = NULL;
	//销毁尾结点
	free(ptail);
	ptail = NULL;
}

头删

//头删
void SLTPopFront(SLTNode** pphead) {
	assert(pphead);
	//链表不能为空
	assert(*pphead);

	//让第二个节点成为新的头
	//把旧的头结点释放掉
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

删除

删除又分为删除指定结点删除指定结点之后的所有结点

删除pos结点

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos) {
	assert(pphead);
	assert(*pphead);
	assert(pos);

	//pos刚好是头结点,没有前驱节点,执行头删
	if (*pphead == pos) {
		//头删
		SLTPopFront(pphead);
		return;
	}

	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	//找到了prev以及pos pos->next
	//先改变指向
    prev->next = pos->next;
	//再释放节点
    free(pos);
	pos = NULL;
}

删除pos之后的结点

//删除pos之后的节点,也就是pos->next->next
void SLTEraseAfter(SLTNode* pos) {
	assert(pos);
	//pos->next不能为空
	assert(pos->next);

	//pos  pos->next  pos->next->next
	SLTNode* del = pos->next;//
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

销毁

//销毁链表
//注意链表的空间是不连续的,所以不能一次性销毁所有元素,只能使用循环一个元素一个元素销毁
void SListDesTroy(SLTNode** pphead) {
	assert(pphead);
	assert(*pphead);

	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

以上操作都基于单链表

部分其他链表的代码实现

循环链表

在介绍链表的分类的时候,已经介绍了循环链表的基本定义,也就是链表的最后一个结点的指针域指向第一个结点,这样就形成了一个循环链表,如果用图像来表示关系的话,如下:
在这里插入图片描述

那么针对其代码的实现,实际上只需要让最后一个结点的指针域不指向空,而是指向第一个结点即可。

//关键代码
p=B->next->next;
B->next=A->next;
A->next=p;//指向头结点

循环链表的作用以及使用场景

  1. 约瑟夫问题:约瑟夫问题是一个经典的问题,即有n个人围成一圈,从第一个人开始报数,报到m的人出列,然后从出列的下一个人开始重新报数,直到所有人都出列。循环链表可以很好地模拟这个问题。
  2. 环形队列:循环链表可以用来实现环形队列,即队列的尾节点指向头节点,可以很方便地实现循环入队和出队操作。
  3. 循环播放列表:循环链表可以用来实现循环播放音乐列表或视频列表等功能。实际上循环这个概念,在生活中许多部分都有被使用到,而当它需要使用代码实现的时候,那么循环链表是较为容易实现的方案。

双向链表

在这里插入图片描述

双向链表的每个节点都包含两个指针,一个指向前一个节点,一个指向后一个节点。鉴于这个特点,它与单向链表不同的是,双向链表可以从头到尾或从尾到头遍历链表。

在双向链表中,通常有一个头节点和一个尾节点,它们分别指向链表的第一个节点和最后一个节点,可以方便地在头部或尾部进行插入或删除操作。

双向链表的操作普遍上比单向链表简单,因为它多了一个指针域所以操作的灵活性大大提高。

双向链表的作用以及使用场景

  1. 需要频繁在链表中间插入或删除节点的情况:双向链表可以在O(1)的时间复杂度内完成插入或删除操作,因此适合在需要频繁插入或删除节点的场景中使用。
  2. 需要双向遍历链表的情况:双向链表可以方便地从头到尾或从尾到头遍历链表,因此适合在需要双向遍历链表的场景中使用。
  3. 需要实现栈或队列的情况:双向链表可以方便地在两端进行插入或删除操作,因此适合用来实现栈或队列。
  4. 需要实现LRU缓存淘汰算法的情况:LRU缓存淘汰算法中经常需要删除最近最少使用的节点,双向链表可以方便地删除尾节点,因此适合用来实现LRU缓存淘汰算法。

优点/缺点(对比顺序表)

优点

1.存储空间充足,对内存利用率高

链表无需像顺序表那样预先分配空间,只要内存空间允许,链表中的元素个数就没有限制,那么这也可以反向说明链表对内存的利用率较高。

2.插入和删除的效率高

不像顺序表那样,在进行插入和删除的时候需要移动整个表,链表可以直接对单个元素进行插入和删除操作。

3.动态性和灵活性

链表的大小可以动态地调整,可以根据需要动态地插入或删除元素,不需要提前指定大小。

4.可以实现高级数据结构

实际上,在后续更高阶的数据结构中,许多结构都是基于链表的。链表可以实现栈、队列、哈希表等高级数据结构,具有很高的灵活性和扩展性。

缺点

1.存储密度小,单个结点有效数据占用空间小

我们发现,链表中的一个结点包含数据域和指针域,但是实际上真正存储了有效元素的只有数据域一部分,那么这就说明了其存储密度小(存储密度=数据元素本身占用的存储量/结点结构占用的存储量)

2.存取元素的效率低

链表不像顺序表那样,是随机存取结构,可以随时存取该位置上的元素;它属于顺序存取结构,只能通过遍历来实现存取元素操作。

3.每存一个数据都要开辟动态空间,增加了内存分配的开销,并可能导致内存碎片化。所以说,动态化既有利也有弊。

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

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

相关文章

SQL Server (MSSQLSERVER) 服务无法启动

解决方法&#xff1a; 打开服务&#xff0c;右键SQL Server (MSSQLSERVER) ->属性->登录&#xff0c;改为本地系统用户

OpenGL入门第四步:摄像机视角变换与交互

OpenGL入门第一步:创建窗口、重写虚函数-CSDN博客 OpenGL入门第二步:颜色、纹理设置(解析)-CSDN博客 OpenGL入门第三步:矩阵变换、坐标系统-CSDN博客 目录 函数解析 具体代码 函数解析 相机视角变换需要与鼠标键盘进行交互,需要重写鼠标和键盘响应函数。 初始化 …

如何安装在系统中安装make命令

文章目录 WindowsMacUbuntuCentOS/Red Hat make是系统比较基础的命令&#xff0c;一般会自己携带&#xff0c;如果没有就手动安装一下吧。 Windows 从官网下载 make.exe Make for Windows 官网首页&#xff1a;https://www.gnu.org/software/make/ 下载地址&#xff1a;htt…

mac安装禅道(局域网访问、远程访问)

前提已安装&#xff1a;phpapacheMySQL macOS12 安装 php8.1/apache-CSDN博客 安装MySQL 一、禅道下载 安装官方文档 源码包下载地址&#xff1a;禅道 18.10 从下图可看出&#xff1a;windows和linux一键安装&#xff0c;方便很多 1. 解压禅道源码包 2. 将解压后的文件复制到…

C++基础——继承(上)

一、继承的概念 继承 (inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许实现者保持原有类特性的基础上进行扩展&#xff0c;增加功能&#xff0c;这样产生新的类&#xff0c;称之为派生类&#xff1b; 继承呈现了面向对象程序设计的层次结构…

升级Microsoft 365后,SAP GUI中无法打开Excel的解决方案

最近&#xff0c;我们遇到了一个棘手的问题&#xff0c;一位客户在升级到Microsoft 365后&#xff0c;无法在SAP GUI中打开Excel。这个问题不仅影响了工作效率&#xff0c;也给用户的日常操作带来了不便。在本文中&#xff0c;我们将探讨问题的成因&#xff0c;并提供一种解决方…

2024年【河北省安全员B证】考试及河北省安全员B证复审模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 河北省安全员B证考试参考答案及河北省安全员B证考试试题解析是安全生产模拟考试一点通题库老师及河北省安全员B证操作证已考过的学员汇总&#xff0c;相对有效帮助河北省安全员B证复审模拟考试学员顺利通过考试。 1、…

[vue] nvm

nvm ls // 看安装的所有node.js的版本nvm list available // 查显示可以安装的所有node.js的版本可以在可选列表里。选择任意版本安装&#xff0c;比如安装16.15.0 执行&#xff1a; nvm install 16.15.0安装好了之后。可以执行&#xff1a; …

【2024】最新快手小游戏项目,边玩游戏边赚钱,周赚4000+!快手怎么赚钱,快手干货分享!

大家好&#xff0c;我是一个99年互联网创业者。 相信老朋友对我也不陌生啦&#xff01; 但还是有一些新朋友&#xff0c;在这里我再做个简单的自我介绍吧。 我是做网赚出身的&#xff0c;大大小小的项目都接触过&#xff0c;后面转型做互联网开发程序这块&#xff0c;到现在…

更快!更自然!OpenAI推出GPT-4o,记者实测→

导读&#xff1a;第一财经记者使用GPT-4o来描述图片&#xff0c;发现其生成结果较准确&#xff0c;5秒左右就能生成描述图片的文字。 当地时间5月13日&#xff0c;OpenAI通过直播展示了产品更新。与此前传出的市场消息不同&#xff0c;OpenAI并未推出搜索引擎&#xff0c;也未…

常见加解密算法02 - RC4算法分析

RC4是一种广泛使用的流密码&#xff0c;它以其简洁和速度而闻名。区别于块密码&#xff0c;流密码特点在于按位或按字节来进行加密。 RC4由Ron Rivest在1987年设计&#xff0c;尽管它的命名看起来是第四版&#xff0c;实际上它是第一个对外发布的版本。 RC4算法的实施过程简洁…

OpenText ETX 助力 SMS 集团提高生产力、降低成本并实现全球协作

OpenText ETX 助力 SMS 集团提高生产力、降低成本并实现全球协作 SMS 集团存在的挑战 需要一个可以在全球范围内轻松访问的解决方案&#xff1b;需要一个系统&#xff0c;能够无缝运行图形要求苛刻的基于服务器的应用程序&#xff1b; 结果 1、通过全球用户访问数据&#x…

el-table组件选中后使用toggleRowSelection无法取消已选中的数据——bug记录-骚操作解决

先说本文重点解决的问题&#xff1a; 存在的问题&#xff1a;当右侧已选中的数据中&#xff0c;删除了左侧其他页面的数据&#xff0c;但是左侧数据切换到其他页面后&#xff0c;左侧还保留选中的状态。 最近在写后台管理系统的时候&#xff0c;遇到一个需求&#xff1a; 左…

鬼畜作品创作必备素材,鬼畜自学语音包合集

一、素材描述 鬼畜是什么&#xff1f;鬼畜是一种网络流行语&#xff0c;也是网络文化的一种表现形式。它指的是将原本无关的两个或多个视频、音频、图片或文字进行剪辑、混合、重组等处理后&#xff0c;形成一种新的有趣、诙谐或恶搞的作品。鬼畜的制作过程通常需要一定的技术…

使用PyQt5设计订单查询界面—了解界面布局2

想要实现的界面效果 增加Tab Widge的页签 在MainWindow窗口中选中水平布局&#xff0c;将一个Label控件和一个默认自带两个页签的Tab Widget控件放到水平布局中&#xff0c;Tab Widget控件右键选择“插入页”再选择“在当前页之后”增加页签。 为每一个Tab页签界面都选择“栅格…

【学习笔记】C++每日一记[20240513]

简述静态全局变量的概念 在全局变量前加上static关键字&#xff0c;就定义了一个静态全局变量。通常情况下&#xff0c;静态全局变量的声明和定义放在源文件中&#xff0c;并且不能使用extern关键字将静态全局变量导出&#xff0c;因此静态全局变量的**作用于仅限于定义静态全…

【driver6】debugfs,性能优化,

文章目录 1.内核调试手段&#xff1a;debugfs.h中api建立目录/sys/kernel/debug2.性能优化&#xff1a;裸磁盘无法使用&#xff0c;一般都刷文件系统。驱动加上要考虑磁盘io&#xff0c;内存占用&#xff0c;cpu使用情况3.Valgrind内存泄漏排查案例&#xff1a;4.cpu瓶颈&#…

ArrayList的深拷贝与浅拷贝

1、深拷贝 通过以下代码进行理解 import java.util.ArrayList; import java.util.List;public class Demo {public static void main(String[] args) {List<Integer> c new ArrayList<>();c.add(1);c.add(2);c.add(3);List<Integer> c1 new ArrayList<…

部署Discuz论坛项目

DIscuz 是由 PHP 语言开发的一款开源社交论坛项目。运行在典型的LNMP/LAMP 环境中。 安装MySQL数据库5.7 主机名IP地址操作系统硬件配置discuz-db192.168.226.128CentOS 7-mini-20092 Core/4G Memory 修改主机名用来自己识别 hostnamectl set-hostname discuz-db #重连远程…

海外仓管理优化策略:花更少的钱,收获更大的收益

海外仓成本确实越来越高了。 仓储成本和人力成本几乎占据了海外仓经营成本的一大部分&#xff0c;这严重的影响了海外仓企业的盈利能力。如果你正打算开设海外仓业务或者已经在经营海外仓业务&#xff0c;那这个问题一定不能忽视&#xff0c;毕竟成本越高&#xff0c;就意味着你…