【数据结构初阶】单链表接口实现超详解

news2024/12/23 8:41:46

文章目录

  • 1. 顺序表问题与思考
  • 2.单链表
    • 2. 1 概念与结构
      • 2.1.1 结点
      • 2.1.2 链表的性质
  • 3. 单链表实现
    • 3. 1 单链表初始化
    • 3. 2 单链表的打印
    • 3. 3 头插
    • 3. 4 创建新节点
    • 3. 5 尾插
    • 3. 6 头删和尾删
    • 3. 7 查找
    • 3. 8 在指定位置之后插入或删除
    • 3. 9 在指定位置前面插入或删除指定位置
    • 3. 10 销毁


1. 顺序表问题与思考

上一篇博客中我们介绍了顺序表,那么顺序表有什么缺点呢?

  1. 中间 / 头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

如何解决以上问题呢?
我们可以使用单链表

2.单链表

2. 1 概念与结构

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

链表就像一个火车:
火车淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉加上,不会影响其他车厢,每节车厢都是独立存在的。
在链表里,每节“车厢”是什么样的呢?
链表

2.1.1 结点

与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为“结点”
结点的组成主要有两个部分:当前结点要保存的数据保存下一个结点的地址(指针变量)。
图中指针变量 plist保存的是第一个结点的地址,我们称plist此时指向第一个结点,如果我们希望plist指向第二个结点时,只需要修改plist保存的内容为0x0012FFA0
链表中每个结点都是独立申请的(即需要插入数据时才去申请一块结点的空间),我们需要通过指针变量来保存下一个结点位置才能从当前结点找到下一个结点

2.1.2 链表的性质

  1. 链式机构在逻辑上是连续的,在物理结构上不一定连续
  2. 结点一般是从上申请的
  3. 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续

结合前面学到的结构体和顺序表知识,我们可以给出每个结点对应的结构体代码:

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;		//用于存储数据
	struct SListNode* next;	//指向下一个节点
}SLTNode;

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存**,这块内存不仅要保存整型数据,也需要保存下一个结点的地址**(当下一个结点为空时保存的地址为空)
当我们想要从第一个结点走到最后一个结点时,只需要在当前结点拿上下一个结点的地址就可以了。

3. 单链表实现

不带头单向不循环链表
记得进行良好的文件管理
1

3. 1 单链表初始化

实际上,单链表并不需要一个用于初始化的函数,这是由单链表的性质决定的。
在使用单链表时,需要在 main 函数中创建一个 SLTNode* 的变量,再将它的地址传递给其他函数就可以了。

为什么在最开始要创建一个指针变量
这个问题在后面头插函数中解释。

3. 2 单链表的打印

单链表的底层结构不是数组了,那我们应该怎样进行打印呢?或者说,我们应该怎么遍历单链表?

void SListPrint(SListNode* plist);	//phead是在main函数中创建的变量,是单链表的头

我们可以用一个SLTNode*变量 pcur 来遍历数组,在打印了pcur->data之后,让pcur变为pcur->next,直到cur==NULL结束,就可以了。

void SListPrint(SListNode* plist)
{
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

如图所示:
1

3. 3 头插

void SListPushFront(SListNode** pplist, SLTDateType x); //注意第一个参数是二级指针

要完成头插,我们需要完成几个步骤:

  1. 创建新节点
  2. 将头结点的next指针指向原来的头结点
  3. 将头结点换成这个新插入的节点

首先第一步创建新节点,这个步骤在其他的插入函数中也会用到,所以我们先跳过,稍后单独封装一个函数BuyListNode方便使用。
假设我们得到了新节点newnode,完成第二步就很简单了,只需要newnode->next=*pphead 就可以了。
第三步,pphead是指向头结点的指针,因此要修改头结点,只需要修改*pphead就可以了(这也就是为什么要传递二级指针

参考代码:

void SListPushFront(SListNode** pplist, SLTDateType x)
{
	assert(pplist);		//在函数解引用之前断言它不是空指针
	SListNode* newnode = BuySListNode(x);
	if (*pplist == NULL)
		*pplist = newnode;
	else
	{
		newnode->next = *pplist;
		*pplist = newnode;
	}
}

值得注意的是,最后两步的顺序绝对不可以颠倒

3. 4 创建新节点

创建新节点一般是在插入时调用的,因此将存储数据这一步直接放在函数内部会更方便使用。

SLTNode* BuyListNode(SLTDataType x);

这个函数用来动态开辟一个新节点,注意一定要是动态申请的,不然创建的节点会在函数调用结束后随着函数栈帧一起销毁。

SLTNode* BuyListNode(SLTDataType x)//创建新节点
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (NewNode == NULL)
	{
		printf("malloc failed");
		exit(-1);			//检查malloc是否成功,若失败则报错退出
	}
	newnode->data = x;
	newnode->next = NULL;	//新的节点的next置为NULL,方便使用
	return newnode;
}

除了上面这样使用返回值传递新节点,你当然也可以使用二级指针来传递新节点,就像这样:

void BuyListNode(SLTNode** ppnewnode,SLTDataType x);
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		printf("malloc failed");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	*ppnewnode = newnode;
}

但是这样的话就要在函数调用之前创建SLTNode*类型的变量,不方便使用,所以不推荐这样写。

3. 5 尾插

void SListPushBack(SListNode** pplist, SLTDateType x);

相较于头插,尾插多了一个步骤:找到单链表现在的尾节点
要完成头插,我们需要完成几个步骤:

  1. 创建新节点
  2. 找到尾节点
  3. 将尾结点的next指针指向新节点

我们重点来看第二步,在单链表中,并没有存储尾节点,但是我们可以通过遍历的方式找到尾节点,就像:

SListNode* cur = *pplist;
while (cur->next)
	cur = cur->next;

最终退出循环时,cur就是尾节点了。

当然,值得注意的是,如果此时单链表为空也就是*plist==NULL的时候就不能按第三步进行了,否则会发生空指针的解引用,要单独处理这一情况,将这个新节点变成头结点。

参考代码:

void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newnode = BuySListNode(x);
	if (*pplist == NULL)
		*pplist = newnode;		//如果链表为空,将新节点作为头结点
	else
	{
		SListNode* cur = *pplist;
		while (cur->next)
			cur = cur->next;	//找到尾节点
		cur->next = newnode;
	}
}

3. 6 头删和尾删

头删相对简单,分为两个步骤:

  1. 将原来的头结点存储起来,并将下一个节点作为头结点。
  2. 释放原来的头结点,并将指向原来的头结点的指针置空。

我们来分析一下,要不要对单链表中只有一个节点的情况特殊处理:
1
很显然,不需要,因为头结点的下一个节点就是NULL,那么头结点也就变成了空结点
参考代码:

void SListPopFront(SListNode** pplist)
{
	assert(pplist && *pplist);
	SListNode* tmp = *pplist;
	*pplist = (*pplist)->next;
	free(tmp);
	tmp = NULL;
}

再来看尾删,尾删麻烦一点,分为几个步骤:

  1. 找到尾节点和它的上一个节点。
  2. 释放尾节点。
  3. 将现在的尾节点的next指针置为空。

除此之外,我们也来分析一下如果链表中只有一个节点的情况:
不需要过多分析我们就能发现:在第一步的时候,就无法找到头结点的上一个节点,因此这种情况显然要单独处理:

if ((*pplist)->next == NULL)
{
	SListNode* tmp = *pplist;
	free(tmp);				//不要忘记释放节点,避免内存泄漏
	tmp = NULL;
	*pplist = NULL;
}

那么尾删的代码也就很容易写出来了:

void SListPopBack(SListNode** pplist)
{
	assert(pplist && *pplist);
	if ((*pplist)->next == NULL)
	{
		SListNode* tmp = *pplist;
		free(tmp);
		tmp = NULL;
		*pplist = NULL;
	}
	else
	{
		SListNode* cur = *pplist;
		SListNode* pre = NULL;
		while (cur->next)	//循环会在cur为尾节点时停止
		{
			pre = cur;
			cur = cur->next;
		}
		free(cur);
		cur = NULL;
		pre->next = NULL;	//如果不进行这一步,尾节点就会指向野指针
	}
}

3. 7 查找

SListNode* SListFind(SListNode* plist, SLTDateType x);
//找到存储了数据x的节点,并返回节点没找到就返回NULL

这个是为了后面的删除插入做前置工作。
这个函数只需要遍历一遍链表并一一比较就可以了:

SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

3. 8 在指定位置之后插入或删除

先来看插入

void SListInsertAfter(SListNode* pos, SLTDateType x);

插入有几个步骤:

  1. 申请新节点
  2. 将新节点的next指针指向pos->next
  3. pos->next指向新节点

这个pos来自上面的查找函数,也就是说,如果pos不为空,那么pos是一定在链表中的,不需要额外检查,但需要检查它是否为空
特别要注意的是:第二步和第三步的顺序绝对不能颠倒,不然就会找不到原来的下一个节点

void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);
	SListNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

再来看删除

void SListEraseAfter(SListNode* pos)

删除分为几个步骤:

  1. 将pos->next指向pos->next->next
  2. 释放pos->next

当然,pos也需要进行判空操作,pos->next也需要,要调用这个函数,这两者应该都不能为空。

void SListEraseAfter(SListNode* pos)
{
	assert(pos && pos->next);
	SListNode* tmp = pos->next;
	pos->next = tmp->next;
	free(tmp);
	tmp = NULL;
}

3. 9 在指定位置前面插入或删除指定位置

需要注意的是,在C++的STL库(数据结构库)中,并没有这样的两个函数,至于原因会在最后讲。

先来看插入

void SListInsert(SListNode** pphead, SListNode* pos, SLTDateType x);

分为几个步骤:

  1. 遍历单链表,找到pos的上一个节点
  2. 申请新节点
  3. 将新节点按在指定位置之后插入的方式插入到pos的上一个节点后面
void SListInsert(SListNode** pphead, SListNode* pos, SLTDateType x)
{
	assert(pphead && pos);
	SListNode* newnode = BuySListNode(x);
	SListNode* pre = *pphead;
	while (pre->next != pos)
	{
		pre = pre->next;	//遍历找到pos的上一个节点
	}
	pre->next = newnode;	//剩下的便是在指定位置之后插入了
	newnode->next = pos;
}

再来看删除

void SListErase(SListNode** pphead, SListNode* pos)

分为几个步骤:

  1. 遍历单链表,找到pos的上一个节点
  2. 按在指定位置之后删除的方式删除pos

不过值得注意的是:如果pos是头结点的话,还需要将头结点改变一下,这也是为什么这个函数中头结点要传递二级指针。

void SListErase(SListNode** pphead, SListNode* pos)
{
	assert(pphead && pos);
	if (*pphead == pos)			//处理pphead和pos重合的情况
	{
		*pphead = pos->next;
		free(pos);
		pos = NULL;
	}
	else
	{	
		SListNode* pre = *pphead;
		while (pre->next != pos)
		{
			pre = pre->next;	//找到pos的上一个节点
		}
		pre->next = pos->next;	//这后面的就是在指定位置后面删除节点
		free(pos);
		pos = NULL;
	}
}

为什么C++的STL库不提供这两个函数?
我们可以计算一下这两个函数以及上面的向后插入删除的函数的时间复杂度,可以发现,向后插入删除的函数的时间复杂度都是O(1),而下面这两个函数的时间复杂度都是O(N),因为单链表不能直接找到节点的上一个节点,所以单链表并不适合向前插入和原地删除的函数,有其他的链表可以更好地完成这样的任务

3. 10 销毁

void SListDestroy(SListNode** pphead)

在单链表使用结束后,一定要记得销毁单链表,不然会发生内存泄漏,可能导致程序崩溃。
单链表的销毁比顺序表麻烦一些,因为单链表的空间是一块一块地申请的,所以也要一块一块地释放。
只需要遍历单链表, 逐个释放就可以了。

void SListDestroy(SListNode** pphead)
{
	assert(pphead);
	SListNode* cur = *pphead;
	while (cur)
	{
		SListNode* tmp = cur->next;
		free(cur);
		cur = tmp;		//遍历着往下走
	}
	*pphead = NULL;		//还要记得将头结点置空,不然就是一个野指针
}

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会持续更新更多优质文章

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

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

相关文章

推荐一款好用的mac解压缩软件

文章目录 介绍软件功能安装下载使用注意事项1.打开系统设置选择隐私与安全性2.点击添加扩展3.勾选访达扩展 小结 介绍 FastZip for Mac集压缩、解压、预览、加密压缩、分卷压缩、固实压缩、右键压缩解压、多线程压缩等功能于一体,绝佳的设计、便捷的操作&#xff0…

设计师AI神器!AnyDesign,一张照片加简单描述就可以编辑时尚图像!

在AI时代很多行业都被颠覆了,建议大家在业余时间也尽量多学习一些AI工具的使用,提高效率的同时也去探索更多好玩的应用。 今天给大家介绍一个非常好用的图像编辑方法-AnyDesign,适合时尚设计师以及普通用户使用的,可以让你能够更…

Python编程:从入门到实践书籍介绍

对于Python入门的书籍推荐,以下是五本详细讲解的书籍,它们各自具有不同的特点和适用对象: 1. 《Python编程:从入门到实践》 作者:埃里克马瑟斯(Eric Matthes)《Python编程:从入门到实践》是一本经典的Pyth…

C#实现数据采集系统-数据反写(1)MQTT订阅接收消息

C#实现数据采集系统-数据反写 实现步骤 MQTT订阅,接收消息反写内容写入通信类,添加到写入队列中 链接-消息内容处理和写入通信类队列实现Modbustcp通信写入 具体实现 1.MQTT订阅,接收消息 Mqtt实现采集数据转发 Mqtt控制类增加订阅方法…

【微信小程序】使用 npm 包 - Vant Weapp --定制主题

小程序对 npm 的支持与限制 1. 什么是 Vant Weapp 官方文档地址 :https://youzan.github.io/vant-weapp 2. 安装 Vant 组件库 详细的操作步骤,大家可以参考 Vant 官方提供的快速上手教程: https://youzan.github.io/vant-weapp/#/quickst…

Texio电源维修德士直流电源维修PSW-1080M160

Texio德士电源维修类型有:指针式小型直流电源、数字显示直流电源、多路输出直流电源、直流电源、相控电源、低噪声开关电源、宽幅电源、高电压宽幅电源、开关电源、超薄直流电源、4象限双电源、高电压直流电源 Texio电源维修常见系列如下 PSW-系列是一款单路输出、…

框架——Mybatis(!!!MyBatis 环境搭建步骤)

目录 一、Mybatis 概述 1.背景 2.简介 3.Mybatis 中文官网 二、MyBatis 环境搭建(超全!!!) 1.创建一张表和表对应的实体类 2.导入 MyBatis jar包,mysql数据库驱动包 3.创建 MyBatis全局配置文件 4. 在接口中…

2024年4款高质量的英语翻译工具推荐!

英语作为一门应用非常广泛的语言,其影响力还是非常深远的。虽然现在学英语的人很多,但对于非英语母语的人来说,英语多多少少会是一个壁垒,所以翻译工具也变得重要了起来。这次,我便要跟大家分享几个很专业的英语翻译工…

连锁门店收银系统源码+电子发票

传统纸质开票模式,流程复杂、时间长,为解决商户开票难的问题,千呼新零售2.0上线了电子发票功能,开通方便,使用简便!商户只需要简单配置,就可以实现门店实现开票自由! 一、线下订单开…

黑马程序员|8天Python第13章面向对象

一 初识对象 1.生活中数据的组织 2.程序中数据的组织 3.使用对象组织数据 类的属性: 二 成员方法 1.类的定义和使用 2.成员变量和成员方法 类外面是函数,类里面是方法。 3.成员方法的定义语法 self 只是写在这里,传参的时候可以当作不存在。…

QTCreator学习

1.新建程序 2. 设置项目名称 3. Build System选择qmake,若选择cmake则只会产生CmakeLists文件,不会产生pro文件。 4.Base class选择QDialog,表示该类继承于QDialog类 5.套件选择MinGW 32bit,取消掉其他的。 6. 双击ui文件,拖动可添加工具。 7.点击左…

深信服技术服务工程师面试全过程分享

吉祥知识星球http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247485367&idx1&sn837891059c360ad60db7e9ac980a3321&chksmc0e47eebf793f7fdb8fcd7eed8ce29160cf79ba303b59858ba3a6660c6dac536774afb2a6330#rd 《网安面试指南》http://mp.weixin.qq.com/s?…

这对二婚夫妻结婚半年,一起生活才一个月,就走到了婚姻尽头!

这对二婚夫妻结婚半年,一起生活才一个月,就走到了婚姻尽头! 这是一篇涉离婚纠纷的民事起诉状 (范文点评) 离 婚 起 诉 状 原告:韩某斌,男,现年37岁,汉族,打…

Spring Cloud Consul面试题

​ ​ 您好,我是程序员小羊! 前言 Spring Cloud Consul 是微服务架构中的一个重要组件,用于服务发现、配置管理以及健康检查。了解 Spring Cloud Consul 的工作原理和应用场景,对于微服务开发者和架构师来说至关重要。以下是一些常…

线程优先级调度

Windows优先级调度算法 系统维护了一个全局的处理器数组KiProcessorBlock,其中每个元素对应于一个处理器的KPRCB对象。其次,另有一个全局变量KiIdleSummary记录了哪些处理器当前是空闲的。所谓一个处理器是空闲的,是指该处理器正在执行空闲循…

《Techporters架构搭建》-Day08 Spring Boot日志实现

集成日志功能 日志介绍日志相关概念选择Logback还是Log4j2?LogBack相关知识LogbackLogback的基本概念Logback的日志级别Logback的配置文件Logback日志文件解析logback-spring.xml的配置项标签说明完整的logback-spring.xml配置示例知识点补充 整合Spring Boot和Logb…

【Lecture1】清华大学大模型公开课——大模型绪论

#清华大模型公开课第二季 #OpenBMB 目录 1. The Evolution of Artificial Intelligence --History 人工智能的演变--历史 1.1 Definition of AI --定义 1.2 Conceptualization of AI -- 概念 1.3 Birth of AI as a Discipline 1.4 Development of AI 1.4.1 Symbolic Int…

Oracle问题笔记

ORA-28040 没有匹配的验证协议 问题出现场景oracle数据库为12c,应用使用的jdbc或客户端工具是11g版本一下,连接12c数据库时会报ora-28040错误。解决办法在Oracle服务端的$ORACLE_HOME/network/admin/sqlnet.ora文件中添加: SQLNET.ALLOWED_LOGON_VERSI…

消息队列篇

1、队列简介 概念: 队列是任务到任务、任务到中断、中断到任务数据交流的一种机制,说白了,队列就是用来传递消息的。 ----------------------------------------------------------------------------------------------------------------…

【Python机器学习】NLP概述——深度处理

自然语言处理流水线的各个阶段可以看作是层,就像是前馈神经网络中的层一样。深度学习就是通过在传统的两层机器学习模型架构(特征提取建模)中添加额外的处理层来创建更复杂的模型和行为。 上图中,前四层对应于聊天机器人流水线中的…