数据结构篇:线性表的另一表达—链表之单链表(上篇)

news2025/4/18 9:10:03

目录

1.链表的引入

1.1 链表的概念

1.2 next的意义

2.链表的分类

3.单链表的实现

3.1 单链表实现接口        

3.1.1 插入节点函数封装     

3.1.2 尾插

3.1.3 头插

3.1.4 报错的根本问题

3.1.5 头删

3.1.6 尾删

4.小结        


1.链表的引入

        根据顺序表的一些缺陷,引出一个线性表的另外一种结构——线性表,可以弥补顺序表如下的不足:

  1. 插入和删除数据,要多次移动元素。
  2. 空间上消耗比较大,由于是增容性质,每次扩容需要开辟新的空间并且拷贝数据,内存消耗大,且难以避免空间浪费。

        那么我们就会引入链表,链表相较于顺序表到底有哪些优势?又有哪些劣势,本次blog带你一探究竟,但是可以说,链表和顺序表没有那个更好,只是在实际应用中由于特性不同,所以使用场景不同。试想一下,如果这两个有哪个更好的说法,那不是另外一个要被替代了,很显然不是。

1.1 链表的概念

        顺序表的物理结构是连续的,这个我已经在上次顺序表篇讲过且实机演示过了。那么链表呢?其实链表在物理结构空间上是不连续的,非顺序的,但是为了方便构思,链表在逻辑结构上是连续的,是通过链表中的指针连接实现的。这是自己画的逻辑图和物理结构图:

链表物理结构和逻辑结构

        从上图可以看出

  1. 链式结构的物理地址是不一定连续的,虽然逻辑结构上是连续的
  2. 链式结构的节点是从堆上申请的空间
  3. 堆上的空间,是按照动态内存管理分配的,所以根据其动态性质,再次申请的空间,通常情况下也是非连续的

可能不是那么好理解,大概率是要调试后才明白,那就来调试一下链表的物理结构到底连不连续,看数据说话:

单链表元素1的地址
单链表元素2的地址
单链表元素3的地址

         那么从前面三个数据1,2,3的物理地址可以看出来,0x00b06300,0x00b06338,0x00b040d0

        不符合一个整形4个字节的内存分布,是不连续的,是无序的,而且差距有的小,有的大。(这里是在监视窗口看的,也可以在内存中看,不过要F11进入函数内部然后看你对应数据的变量,&xxx->data,回车查看,一定要取地址),比如博主的节点数据定义是newnode。

        而逻辑结构就比较清晰,是连续的。但是本文的重中之重来了,有没有发现物理结构中,数据都是分上下两层


        1下面的物理地址和2上面物理地址一样,2下面的地址和3上面的一样。


并且逻辑结构中A后面有个小空间是空的,是next,next就是本文的重点了,理解了next,链表的基本结构就是了解了。博主一开始也是一知半解,看了其他博主的相关博客和写了一些牛客力扣题后才逐渐明白并且理解运用。

1.2 next的意义

        next是下一个的意思,简单的理解为在上一个节点/尾节点存放下一个/新尾节点的地址。记住,next是在原来节点的。再次用一下上表的调试图

        图中的data=1中的next是不是6338。而next对应的数字是data=2。data=2时,确实物理地址也是6338。


        第二个next是49d0,对应的数字是data=3,3的物理地址就是40d0。这也很好的解释了原节点next存放的是下一个data=2的地址。这样子讲应该可以理解吧。这里是本文的第一个重点!是全文的铺垫内容。

2.链表的分类

        链表从有头,无头;双向,单向;有无循环分为8种,分别是有头双向循环,有头双向无循环;有头单向循环,有头单向无循环;无头双向循环,无头双向无循环;无头单向循环,无头单向无循环。

有无头节点,单双向
是否循环

        但是常见的是两类,不带头单向非循环:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构。

不带头单向非循环

        带头双向循环: 结构复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。


另外这个结构虽然结构看着复杂,但是知道单链表怎么运用,手撕双链表代码会发现结构会带来很多优势,反而简单了,后面我们代码实现了就知道了。所以先看单链表。

带头双向循环

3.单链表的实现

        无头单向非循环链表,也叫单链表,单链表的实现的形式也是增删查改。那么单链表结构体中需要有数据,还有下一个节点。所以需要去定义。

3.1 单链表的定义

        为了方便和顺序表的int区别开,int重命名为Sltdatatype,然后定义一个data和下一个节点。

typedef int Sltdatatype;
typedef struct SListnode
{
	Sltdatatype data;
	struct SListnode* next;
}SLTnode;

3.1 单链表实现接口        

        之后便是我们熟悉的给节点动态申请空间(一个节点申请一个空间)增删查改,打印,销毁,那么自然少不了头插尾插,头删尾删和中间位置删除和插入。 这里没有初始化的原因是因为单链表就一个data值,不像顺序表那样有多个值,所以不需要初始化。

void SLTprint(SLTnode *phead);

SLTnode* insertSLTnode(Sltdatatype x);

void SLTpushback(SLTnode* pphead, Sltdatatype x);

void SLTpopback(SLTnode* pphead);

void SLTpushfront(SLTnode* pphead, Sltdatatype x);

void SLTpopfront(SLTnode* pphead);

SLTnode* SLTfind(SLTnode* pphead, Sltdatatype x);
//pos之前插入
void SLTinsert(SLTnode *pphead,SLTnode*pos,Sltdatatype x);

//pos位置删除
void SLTErase(SLTnode* pphead, SLTnode* pos);

//pos后面插入
void SLTinsert_afterpos(SLTnode*phead,SLTnode* pos,Sltdatatype x);

//pos后面删除
void SLTerase_afterpos(SLTnode* pphead, SLTnode* pos);
//链表销毁
void SLTdestory(plist);

3.1.1 插入节点函数封装     

        和之前一样,插入的地方都要用到插入的函数,那干脆就单独封装成一个函数,好处就不再强调了。插入函数,就是申请一块结构体大小的空间给新节点,这个结构体的类型是SLTnode。申请成功的话就把要插入的数赋值给新节点的data,然后新节点存放下一个地址的next置空。

SLTnode* insertSLTnode(Sltdatatype x)
{
	//申请一块空间给新的节点
	SLTnode* newnode = (SLTnode*)malloc(sizeof(SLTnode));
	if (newnode == NULL)
	{
		perror("fail malloc");
		return 0;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

        完成了这个插入函数的撰写,那么就可以进行尾插头插等工序了。

3.1.2 尾插

        要知道,一开始肯定是链表肯定是没有数据,是空的。所以当链表是空的时候,就让pphead指向新节点。之后每次就找最后一个节点的位置,也称为尾节点,尾节点怎么找呢,可以想象一下这个节点的下一个为空,那么该节点就是最后一个节点,所以用循环。不为空就表示这个节点下面最少还有一个,如此即可。这里我画了几个接口实现的逻辑图,便于自己理解,先画图再敲代码

单链表各部分操作逻辑图

        找到后进行关键一步的操作,也就是next的链接作用。找到尾节点,然后尾节点的next指向新节点。按照上面的说法,也可以叫尾节点的next存放新节点的地址。

void SLTpushback(SLTnode *pphead,Sltdatatype x)
{
	assert(pphead);
	SLTnode* newnode = insertSLTnode(x);
	if (pphead==NULL)
	{
		pphead = newnode;
	}
	//找尾
	else
	{
		//定义尾变量
		SLTnode* tail = pphead;
		while (tail->next!=NULL)
		{
			tail = tail->next;
			
		}
		//tail->next这个指针指向新节点
		tail->next= newnode;
	}
	
}

3.1.3 头插

        同尾插一样是,一开始链表没有数据的情况下,也可以不单独判断,因为即使空指针,新节点的next指向*pphead,这个*pphead目前是NULL啊。然后plist指向newnode,或者说newnode赋值给*pphead,变成新头了。始终记住*pphead是plist的值


        之后就要把newnode新节点给传过来的指针之后每次在节点之前进行头插。按照逻辑图的思路,是newnode的下一个也就是next指向目前plist的第一个值。然后plist指向新节点,从而使新节点变成新的头部。

void SLTpushfront(SLTnode* pphead, Sltdatatype x)
{
	assert(pphead);
	SLTnode* newnode = insertSLTnode(x);
	//把newnode的下一个给pilst
	newnode->next = pphead;
	//链表指针指向新节点,变成新的头部
	pphead =newnode;
}

         接下来,我们来局部测试一下看看是否正确,写一个测试用例


void test()
{
	SLTnode* plist = NULL;

	SLTpushback(plist, 1);
	SLTpushback(plist, 2);
	SLTpushback(plist, 3);
	SLTpushback(plist, 4);
	SLTpushfront (plist, 5);
	SLTprint(plist);
int main()
{
test();
return 0;
}

        断言表示,错误在Slist.c第24行,让我们看看错误。这是为什么呢?        调试可知,传过来的pphead是NULL的,空的报错也无可厚非了。其实当时的我遇到这个问题也是百思不得其解。感觉我操作的没问题,起码思路上面没有任何毛病。这让我很头疼,大概过了两三天,依旧是搜索了其他博主关于线性表,链表实现头插尾插的各种细节,包括头文件声明,测试部分和具体实现文件上,也问了AI,最后得以找到了我的问题所在,以便让我再次对指针有了新的认知。

3.1.4 报错的根本问题

         错误的原因同样也是这篇文章的第二个关键点,我在这里犯了一个认知型错误,究其原因就是对指针的了解还不够。


首先形参是实参的拷贝,形参的改变不影响实参


而且形参出了作用域就销毁了。先讲个样例展示我的错误。

void fun(int y )
{
    y=10;
}
int main()
{
    int x =2;
    fun(x);
    return 0; 
}

y的改变肯定是不会改变x的。要修正

void fun(int *p )
{
    *p=10;
}
int main()
{
    int x =2;
    fun(&x);
    return 0; 
}

这样子才能改变,因为改变int要用int *指针,画个图说明就是

传址

        p不是x的值的拷贝,而是x的地址的拷贝,是把x的地址存放在了p指针里。使用*解引用,找到x,并且改掉。把这个写法换到我们这次的错误上并简化就是:

void Func(int *ptr)
{
    ptr=(int*)malloc(sizeof(int));
}
int main()
{
    int *p =NULL;
    Func(p);
    return 0;
}

        这是把p的值传给了ptr,然后ptr申请了一块空间,指针ptr指向空间,但是ptr的改变没有影响到px,出了作用域销毁了。

传值

        真相就是改变指针变量的形参,同样不会影响实参。改指针的时候给一个函数传指针本身是无效的,需要传的是指针的指针,即二级指针。通俗讲,改变int,要传int*指针,改变int*,传int**指针。改正之后就是如下代码:

void Func(int **ptr)
{
    *ptr=(int*)malloc(sizeof(int));
}
int main()
{
    int *p =NULL;
    Func(&p);
    free(p)
    return 0;
}

       把p的地址传过去,ptr存放p的地址,也就是指针的地址,二级指针,*ptr就是p的内容,从而达到在函数内部改变实参p的效果:p指向申请的空间。总结改变int*,传int*的地址,用int**。


其实自己犯的错误就是错误使用指针了。所以上文的错误就是我要改变的plist是一个一级指针,所以一开始传的plist本身没有任何效果,需要传二级指针,实参部分要取plist的地址,存放在SLTnode* *pphead里。*pphead的值就是plist的值。

         那么修正好我们的头插尾插的代码后就是

void test()
{
	SLTnode* plist = NULL;
	SLTpushback(&plist, 1);
	SLTpushback(&plist, 2);
	SLTpushback(&plist, 3);
	SLTpushback(&plist, 4);
	SLTpushfront(&plist, 5);
	SLTprint(plist);
}
int main()
{
	
	test();
	return 0;
}
//尾插
void SLTpushback(SLTnode **pphead,Sltdatatype x)
{
	assert(pphead);
	SLTnode* newnode = insertSLTnode(x);
	if (*pphead==NULL)
	{
		*pphead = newnode;
	}
	//找尾
	else
	{
		//定义尾变量
		SLTnode* tail = *pphead;
		while (tail->next!=NULL)
		{
			tail = tail->next;
			
		}
		//tail->next这个指针指向新节点
		tail->next= newnode;
	}
	
}
void SLTpushfront(SLTnode** pphead, Sltdatatype x)
{
	assert(pphead);
	SLTnode* newnode = insertSLTnode(x);
	//把newnode的下一个给pilst
	newnode->next = *pphead;
	//链表指针指向新节点,变成新的头部
	*pphead =newnode;
}

        运行结果,运行成功!

         这个问题如果对指针的用法不是很掌握的话,就会和我一样,踩这个大坑。所以这个传指针也是个坑,而且后面还会考到就是二级指针的用法。

3.1.5 头删

        清楚了头插的错误并且改正,那么头删的部分就比较好理解了,首先要改变plist指针的内容,改变int*用int**二级指针。并且进行断言。因为pphead是plist的地址,一定不会为空,为空就像我犯的错误一样,传错了。这个断言问题后面会单独介绍,因为同样重要

        看上文的思路图,把*pphead就是plist的值给first指针,在我的理解就是让first代替plist操作,此时first指向plist的第一个节点。之后让*pphead指向first下一个节点,就是第二个节点。意味着plist指向第二个了。如果只有一个节点,就直接释放掉。

        代码如下:

void SLTpopfront(SLTnode** pphead)
{
	assert(pphead);
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	
	//第一个节点的内容,给到first这个指针
		SLTnode* first = *pphead;
		//plist指向first的下一个地址,就是第二个节点
		*pphead = first->next;
		free(first);
		//删除第一个
		first = NULL;
}

3.1.6 尾删

        根据最上面的思路图可以知道,尾删需要释放掉最后一个节点,需要定义一个前指针保存尾巴的前一个,用来更新指针位置。这里依旧需要断言,而且不止检查是否穿错指针,要检查plist的内容,考虑如果是空链表就不能删了,空的删了不就出错了吗。只有一个节点的话直接释放掉就可以 了。

void SLTpopback(SLTnode** pphead)
{
	//检查
	assert(pphead);
	assert(*pphead);
	//一个节点
	if ((*pphead)->next==NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//多个节点
	else
	{
		// 找尾
		SLTnode* former = NULL;
		SLTnode* tail = *pphead;
		while (tail->next != NULL)
		{
			former = tail;
			tail = tail->next;
		}

		free(tail);
		
		tail = NULL;
		former->next = NULL;

	}
}

4.小结        

        讲到这里就先停了,本文构思了很久,消化一下。头插尾插和头删尾删讲完,最重要的两个部分是链表中对于next的理解和一级指针,二级指针在传参时候的区别。接着引入链表的优势答案在这自然也就能体会到了,链表的修改元素比顺序要方便很多,不需要遍历,只需要改变指针指向,并且它是没有顺序表所谓的空间容量概念,不需要空间,避免了空间浪费。

        然后后面的函数:中间某个位置之前插入/某个位置删除/某个位置之后插入/某个位置后面删除逻辑是差不多的。而且特殊位置的话和头删尾删/头插尾插效果是一样的,可以复用。

        该位置是第一个的话,位置之前插入,不就等于头插吗?位置删除就等于头删

        该位置是最后一个的话,某个位置之后插入,就是尾插;倒数第二个位置后面删除就等于尾删。那么想要实现以上的函数可以用这几个函数复用,简洁了许多不是吗?关于其他的销毁函数,查找函数和断言问题(有点绕,要了解一级二级指针的情况下结合实际情况实际考虑)。下文继续~~,感谢观看。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot企业级开发之【用户模块-获取用户详细信息】

接口文档的要求: 了解一下token令牌头是怎么用的 我们直接放到前端交互的controller类下,在声明的方法中加入参数为String token且加入注解RequestHeader(name"Authorization【你自己设定的token】") 设计思路: 实战开发: control…

Mockito如何对静态方法进行测试

在 Mockito 中,直接对静态方法进行模拟是困难的,因为 Mockito 的设计理念是优先通过依赖注入(DI)管理对象,而静态方法破坏了这种设计(难以解耦)。不过,从 Mockito 3.4.0 版本开始,通过 mockStatic 方法支持了对静态方法的模拟(需配合 mockito-inline 依赖)。 从 Mo…

Navicat 17 for Mac 数据库管理

Navicat 17 for Mac 数据库管理 一、介绍 Navicat Premium 17 for Mac是一款专业的数据库管理工具,适用于开发人员、数据库管理员和分析师等用户。它提供了强大的数据管理功能和丰富的工具,使用户能够轻松地管理和维护数据库,提高数据处理效…

grok 驱动级键盘按键记录器分析

grok是一个驱动模块,其主要功能就行进行键盘按键及剪切板数据的记录,也就是一个键盘记录器。实现原理是通过对shadow-ssdt的相关函数进行hook,和r3对GetUserMessage进行hook的原理差不多。 关键部分如下: 查找csrss.exe进程是否已经启动&…

MyBatis中特殊符号处理总结

前言 MyBatis 是一款流行的Java持久层框架&#xff0c;广泛应用于各种类型的项目中。因为我们在日常代码 MyBatis 动态拼接语句时&#xff0c;会经常使用到 大于(>,>)、小于(<,<)、不等于(<>、!)操作符号。由于此符号包含了尖括号&#xff0c;而 MyBatis 使用…

MYSQL——SQL语句到底怎么执行

查询语句执行流程 MySQL 查询语句执行流程 查询缓存&#xff08;Query Cache&#xff09; MySQL内部自带了一个缓存模块&#xff0c;默认是关闭的。主要是因为MySQL自带的缓存应用场景有限。 它要求SQL语句必须一摸一样表里面的任何一条数据发生变化时&#xff0c;该表所有缓…

智能血压计WT2801芯片方案-BLE 5.0无线传输、高保真语音交互、LED显示驱动、低功耗待机四大技术赋能

在智能健康设备飞速发展的今天&#xff0c;血压计早已不再是简单的“测量工具”&#xff0c;而是家庭健康的“智能管家”。然而&#xff0c;一台真正可靠、易用、功能全面的血压计&#xff0c;离不开一颗强大的“核心芯片”。 今天&#xff0c;我们揭秘医疗级芯片WT2801的硬核实…

基于51单片机的智能火灾报警系统—温度烟雾检测、数码管显示、手动报警

基于51单片机的火灾报警系统 &#xff08;仿真&#xff0b;程序&#xff0b;原理图&#xff0b;设计报告&#xff09; 功能介绍 具体功能&#xff1a; 由51单片机MQ-2烟雾传感ADC0832模数转换芯片DS18B20温度传感器数码管显示按键模块声光报警模块构成 具体功能&#xff1a;…

指定运行级别

linux系统下有7种运行级别,我们需要来了解一下常用的运行级别,方便我们熟悉以后的部署环境,话不多说,来看. 开机流程&#xff1a; 指定数级别 基本介绍 运行级别说明: 0:关机 相当于shutdown -h now ⭐️默认参数不能设置为0,否则系统无法正常启动 1:单用户(用于找回丢…

Python标准库:sys模块深入解析

sys模块是Python标准库中一个非常重要的内置模块&#xff0c;它提供了与Python解释器及其环境交互的多种功能。本文将深入探讨sys模块的各个方面&#xff0c;帮助开发者更好地理解和利用这个强大的工具。 1. sys模块概述 sys模块提供了对由解释器使用或维护的变量的访问&…

加油站小程序实战教程10开通会员

目录 1 修改用户登录逻辑2 创建变量3 调用API总结 我们上一篇搭建了开通会员的界面&#xff0c;有了界面的时候就需要加入一些逻辑来控制界面显示。我们的逻辑是当用户打开我的页面的时候&#xff0c;在页面加载完毕后调用API看用户是否已经开通会员了&#xff0c;如果未开通就…

没有他的“变换”,就没有今天的人工智能

从ChatGPT发布以来&#xff0c;大语言模型&#xff08;LLM&#xff09;是所有人追逐的方向&#xff0c;无论是将其看作“万能神”或是人工智能应用的基础构件&#xff0c;其重要性毋庸置疑。而随着大语言模型扩展到多模态领域&#xff0c;就需要更多的工具来帮助其进行处理。 例…

MCP 实战:实现server端,并在cline调用

本文动手实现一个简单的MCP服务端的编写&#xff0c;并通过MCP Server 实现成绩查询的调用。 一、配置环境 安装mcp和uv, mcp要求python版本 Python >3.10; pip install mcppip install uv 二、编写并启用服务端 # get_score.py from mcp.server.fastmcp import…

关于C++日志库spdlog

关于C日志库spdlog spdlog是一个高性能、易于使用的C日志库&#xff0c;广泛应用于现代C项目中。它支持多线程、异步日志记录、多种日志格式、以及灵活的输出方式&#xff08;如控制台、文件、甚至自定义输出&#xff09;。下面将就常用功能方面介绍spdlog的安装、配置和使用方…

回归预测 | Matlab实现RIME-CNN-GRU-Attention霜冰优化卷积门控循环单元注意力机制多变量回归预测

回归预测 | Matlab实现RIME-CNN-GRU-Attention霜冰优化卷积门控循环单元注意力机制多变量回归预测 目录 回归预测 | Matlab实现RIME-CNN-GRU-Attention霜冰优化卷积门控循环单元注意力机制多变量回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现RIME…

液氮恒温器是做什么的

‌液氮恒温器‌是一种利用液氮作为冷源的恒温装置&#xff0c;主要用于提供低温、恒温或变温环境&#xff0c;广泛应用于科研、工业和医疗等领域。液氮恒温器通过液氮的低温特性来实现降温效果&#xff0c;具有效率高、降温速度快、振动小、成本低等优点。 液氮恒温器应用场景和…

`mpi4py` 是什么; ModuleNotFoundError: No module named ‘mpi4py

mpi4py 是什么 目录 `mpi4py` 是什么ModuleNotFoundError: No module named mpi4pyModuleNotFoundError: No module named mpi4py mpi4py 是一个 Python 模块,它提供了对 MPI(Message Passing Interface)标准的接口,使得 Python 程序能够利用 MPI 进行并行计算。其作用主要…

大数据 - 1. 概述

早期的计算机&#xff08;上世纪70年代前&#xff09; 是相互独立的&#xff0c;各自处理各自的数据上世纪70年代后&#xff0c;出现了基于TCP/IP协议的小规模的计算机互联互通。上世纪90年代后&#xff0c;全球互联的互联网出现。当全球互联网逐步建成&#xff08;2000年左右&…

Java基础下

一、Map Map常用的API //map常用的api//1.添加 put: 如果map里边没有key&#xff0c;则会添加&#xff1b;如果有key&#xff0c;则会覆盖&#xff0c;并且返回被覆盖的值Map<String,String> mnew HashMap<>();m.put("品牌","dj");m.put("…

数据结构和算法(十二)--最小生成树

一、有向图 定义: 有向图是一副具有方向性的图&#xff0c;是由一组顶点和一组有方向的边组成的&#xff0c;每条方向的边都连着一对有序的顶点。 出度: 由某个顶点指出的边的个数称为该顶点的出度。 入度: 指向某个顶点的边的个数称为该顶点的入度。 有向路径: 由一系列顶点组…