第三章 单向链表的讲解与实现

news2024/11/24 17:38:33

初阶数据结构

第一章 时间复杂度和空间复杂度
第二章 动态顺序表的实现
第三章 单向链表的讲解与实现


文章目录

  • 初阶数据结构
  • 前言
  • 一、什么是链表?
  • 二、节点的定义:
  • 三、单向链表接口函数
    • 1、打印:
    • 2、尾插:
    • 3、头插:
    • 4、尾删:
    • 5、头删:
    • 6、查找:
    • 7、插入:
      • (1)向前插入:
      • (2)向后插入:
      • (3)两者对比:
    • 8、删除:
    • 9、销毁:


前言

学习完顺序表之后,我们发现顺序表存在一定的缺陷和问题,尤为突出的问题就是插入和删除时时间的消耗以及开辟内存的时候空间的损耗。那么本章节所学习的链表则能有效地解决这些问题。


一、什么是链表?

链表:顾名思义就是将一个个数据连接起来,从而构成一个数据结构,下图中即为链表的逻辑结构与物理结构。
在这里插入图片描述

每一个链表都是由一节节的链结组成的,我们称之为节点。其中,每一个节点都是由两部分组成的,存储数据的部分叫做数据域,存储地址的部分叫做指针域。指向第一个节点的指针称之为头指针
那么上述的单向链表如何实现呢?

二、节点的定义:

其定义如下:

typedef int ElementType;
typedef struct SListNode
{
	ElementType data;
	struct SListNode* next;
}SLTnode;

三、单向链表接口函数

1、打印:

我们创建一个cur指针,这个指针的目的就是去遍历每一个链表中的节点,然后判断这个节点是否为空。为了方便起见,我们下图的图示采用链表的物理结构,并将最后的空指针也假设一块虚拟的空间,这样便于我们理解。下图的逻辑我们即可写出打印链表元素的代码。
在这里插入图片描述

void SListPrint(const SListNode* phead)
{
	if (phead != NULL)
	{
		SListNode* cur = phead;
		while (cur != NULL)
		{
			printf("%d->", cur->data);
			cur = cur->next;
		}
		printf("NULL\n");
	}
	else
	{
		printf("Empty List!\n");
	}

}

2、尾插:

尾插的实现逻辑如下,我们将新节点中的地址设置为空指针,然后将其前面的指针指向新的节点,即完成了尾插。
在这里插入图片描述
我们这里要分为两种情况。
第一种:链表为空
当链表为空的时候,我们的plist指针指向的是空,此时我们是不能通过->对plist进行解引用的。所以我们需要单独判断一下。
第二种:链表不为空
这种情况下,我们实现尾插的动作一共分为两步,第一步利用tail指针找到尾节点。第二部将新的节点连接到原最后一个节点上。

//2、尾插
void SListPushBack(SListNode** pphead, ElementType dat)
{
	assert(pphead!=NULL);
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		printf("failed to creat space!");
		exit(-1);
	}
	else
	{
		newnode->data = dat;
		newnode->next = NULL;
	}
	
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SListNode* tail=*pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

另外,我们还需要注意一点,就是这里 为什么要用二级指针传入头指针?
我们知道,函数传参分为值传递和地址传递,值传递仅仅是将数据拷贝给了形参,形参发生变化时,实参不会随之而变。而值传递的方式则会引起形参和实参的同步改变。头指针是一个结构体类型的指针,而指针也是一种变量。如果我们将形参设置为一级指针,此时实参和形参的数据类型是一致的,所以发生的是值传递。

假设我们用的是值传递的方式,当链表为空,我们进行尾插的时候,我们会将头指针内的数据改为新节点的地址。但是我们提到了,值传递的方式不会引起实参的变化。因此,当此函数结束后,我们的头指针依旧是空。因此,我们这里需要传入的是二级指针,从而实现地址传递。
在这里插入图片描述

总结为一句话:需要修改链表的时候传入二级指针,不需要修改链表的时候传入一级指针。

3、头插:

头插的逻辑如下图所示,我们将头指针指向我们新的节点,然后将新节点的指针域指向原第一个节点的地址。
在这里插入图片描述
头插的逻辑非常简单,我们将头指针指向新的节点,将新节点的指针域指向原首节点的地址即可。

void SListPushFront(SListNode** pphead, ElementType dat)
{
	assert(pphead!=NULL);
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		printf("Failed to creat new space.\n");
		exit(-1);
	}
	else
	{
		newnode->data = dat;
		newnode->next = *pphead;
		*pphead = newnode;
	}
}

4、尾删:

尾删的逻辑其实很简单,我们只需要将倒是第二个节点的指针域设置为空指针,并且将最后一个节点释放掉。由于我们这种方法都基于两个节点,但是未必所有的链表都有两个节点,因此我们可以将情况分为以下三种:
(1)链表不为空,且元素个数大于等于2
这种情况就是非常普遍的,按照我们刚刚的逻辑模拟即可。要注意的是我们需要提前将倒数第二个位置的地址信息存储起来。
(2)链表为空
这种情况根本不需要尾删操作,直接返回或报错即可。
(3)链表不为空,但是元素只有一个
这种情况不容易想到,但是一但还使用我们刚刚的逻辑,会立马出现访问权限的错误。因为第一个节点就是尾节点,因此他的前面是没有节点的,此时我们是无法按照刚刚的逻辑执行。所以我们需要特判一下,直接将头指针置空,第一个节点释放即可。
在这里插入图片描述

我们根据上面的逻辑可以写出如下代码:

//4、尾删
void SListPopBack(SListNode** pphead, ElementType dat)
{
	assert(pphead!=NULL);
	assert(*pphead!=NULL);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SListNode* tail = *pphead;
		SListNode* pre = NULL;
		while (tail->next != NULL)
		{
			pre = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;
		pre->next = NULL;
	}

}

5、头删:

头删的逻辑就非常简单了,直接将头指针指向原第一个节点的指针域中所对的地址空间即可。
在这里插入图片描述

//5、头删
void SListPopFront(SListNode* *pphead)
{
	assert(pphead!=NULL);
	assert(*pphead!=NULL);
	SListNode* temp = (*pphead)->next;
	free(*pphead);
	*pphead = NULL;
	*pphead = temp;
}

6、查找:

查找很简单,直接遍历。注意返回值返回的是空指针或者所查元素的地址。
在这里插入图片描述

SListNode* SListFind(SListNode* phead,ElementType fin)
{
	assert(phead!=NULL);
	SListNode* cur = phead;
	while (cur != NULL)
	{
		if (cur->data == fin)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}

7、插入:

(1)向前插入:

我们先来大致想一下向前插入的整体逻辑,我们在pos位置前面插入新的节点,只需要如图中所示,找到原前方的节点。然后让该节点指向所插入的节点,然后让所插入的节点指向pos所对的节点。
但是这里我们依旧需要注意一些细节问题:即我们要保证pos前面有节点,否则会再次出现尾删中所出现的访问越界的问题。
从这个问题出发我们就能很轻易地找到特殊情况,即头插。因此我们需要对这种特殊情况进行特判,当出现这种情况时,直接调用头插函数即可。
在这里插入图片描述

void SListInsertFront(SListNode**pphead, SListNode* pos, ElementType dat)
{
	assert(pos != NULL);
	assert(pphead!=NULL);
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		printf("Failed to creat new space!\n");
		exit(-1);
	}
	newnode->data = dat;

	if (pos == *pphead)
	{
		SListPushFront(pphead,dat);
	}
	else
	{
		SListNode* prepos = *pphead;
		while (prepos->next != pos)
		{
			prepos = prepos->next;
		}
		prepos->next = newnode;
		newnode->next = pos;
	}
}

(2)向后插入:

在这里插入图片描述
向后插入就非常简单了,因为我们pos位置所对的节点的指针域中就记录了后面的节点位置。因此我们直接就能插入新的元素,无需遍历寻找。并且不存在头插的特殊情况。

//8、向后插入
void SListInsertAfter(SListNode** pphead, SListNode* pos, ElementType dat)
{
	assert(pos != NULL);
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		printf("Failed to creat new space!\n");
		exit(-1);
	}
	newnode->data = dat;
	newnode->next = pos->next;
	pos->next = newnode;
}

(3)两者对比:

向前插入的时间复杂度是O(N)。向后插入的时间复杂度是O(1)。因此向后插入是更高效的,如果想对向前插入进行算法上的优化的话,我们需要使用双向链表的数据结构。

8、删除:

删除的逻辑如下图所示,我们找到pos位置的前后节点,然后将该两个节点相连即可。但是同样有着头删的问题。所以我们以相同的方式特判一下,其余情况按照下图的逻辑模拟即可。
在这里插入图片描述

void SListErase(SListNode** pphead, SListNode* pos)
{
	assert(pos!=NULL);
	assert(pphead!=NULL);
	assert(*pphead!=NULL);
	if (pos == *pphead)
	{
		SListPopFront(pphead);
	}
	else
	{
		SListNode* prepos = *pphead;
		while (prepos->next != pos)
		{
			prepos = prepos->next;
		}
		prepos->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

9、销毁:

销毁链表的时候,我们不能单纯地像释放顺序表那样只释放头指针。因为链表以这样的方式销毁时,是无法释放干净的,仅能释放头节点。所以我们应该通过下图所示的逻辑通过两个指针去释放每一个节点,最后将头指针设置为空。一定要将头指针设置为空指针!否则会出现野指针的问题!
在这里插入图片描述

void SListDestory(SListNode** pphead)
{
	assert(pphead!=NULL);
	assert(*pphead!=NULL);
	SListNode* cur = *pphead;
	while (cur!=NULL)
	{
		SListNode* temp = cur->next;
		free(cur);
		cur = NULL;
		cur = temp;
	}
	*pphead = NULL;
}

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

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

相关文章

改进YOLOv7系列: 最新结合用于小目标的新CNN卷积构建块

💡统一使用 YOLOv7 代码框架,结合不同模块来构建不同的YOLO目标检测模型。🌟本项目包含大量的改进方式,降低改进难度,改进点包含【Backbone特征主干】、【Neck特征融合】、【Head检测头】、【注意力机制】、【IoU损失函数】、【NMS】、【Loss…

Linux-进程控制

进程控制进程创建fork函数写时拷贝fork常规用法fork调用失败的原因进程终止进程等待进程程序替换程序替换的原理如何程序替换进程创建 fork函数 fork之前父进程独立运行,fork之后,父子两个执行流分别执行。 进程具有独立性,代码和数据必须独立…

机器学习HMM模型

目录1 马尔科夫链1.1 简介1.2 经典举例1.3 小结2 HMM简介2.1 简单案例2.2 案例进阶2.2.1 问题阐述2.2.2 问题解决3 HMM模型基础3.1 什么样的问题需要HMM模型3.2 HMM模型的定义3.3 一个HMM模型实例3.4 HMM观测序列的生成3.5 HMM模型的三个基本问题4 前向后向算法评估观察序列概率…

计算机毕业设计-SSM高校社团招新系统-JavaWeb大学生社团管理系统-源码+文档+讲解

注意:该项目只展示部分功能,如需了解,评论区咨询即可。 本文目录1.开发环境2.系统的设计背景3 前后台功能设计3.1 前台功能3.2 后台功能4 系统页面展示4.1 学生功能模块展示4.2 干部功能模块展示4.3 管理员功能模块展示5 更多推荐6 部分功能代…

如何用IDEA提高你的开发效率

前言 ​ 作为一名java开发工程师,IDEA无疑是我日常接触最多的工具。因此,能否高效使用IDEA软件,一定程度上决定了我们的开发效率。本文将主要介绍IDEA中的四个便于提高开发效率的功能,常用快捷键、实时模版、后缀补全、文件和代码…

《本地计算机DNS缓存文件》

C:\Windows\System32\drivers\etc 36.152.44.95 www.baidu.com 正常访问www.baidu.com可以DNS抓包,将百度的IP及域名加入文件位置的hosts文件中即该IP和域名将不再请求网络上的DNS服务器,即加快域名解析; 具体作用: 1.加快域名解…

什么是RPC框架?

什么是RPC? In distributed computing, a remote procedure call (RPC) is when a computer program causes a procedure (subroutine) to execute in a different address space (commonly on another computer on a shared network), which is coded as if it wer…

创新能力 | 产品经理实践中常犯的七大错误

做产品是一个既感性又理性的过程,纵然有很多前辈同行的经验传承和技巧指导,但在落到实处是时,总难免犯一些错误。有些是经验不足导致,有些则是产品经理对于人性的浅见寡闻。本文作为产品经理实践指南专题的中级篇,阐述…

用 AWTK 和 AWPLC 快速开发嵌入式应用程序 (2)-走马灯

AWPLC 目前还处于开发阶段的早期,写这个系列文章的目的,除了用来验证目前所做的工作外,还希望得到大家的指点和反馈。如果您有任何疑问和建议,请在评论区留言。 1. 背景 AWTK 全称 Toolkit AnyWhere,是 ZLG 开发的开源…

全球名校AI课程库(35)| 辛辛那提大学 · 微积分Ⅱ课程『MATH101 Calculus II』

🏆 课程学习中心 | 🚧 CS数学基础课程合辑 | 🌍 课程主页 | 📺 中英字幕视频 | 🚀 项目代码解析 课程介绍 Trefor Bazett 教授在 Cincinnati 大学任教时,制作了两套完整的的数学课程(微积分、离…

Eclipse创建Servlet项目-7

目录 1、创建动态 Web 项目 2、使用 Eclipse 创建 Servlet 3、配置 web.xml 4、部署项目并启动服务器 通过前面的学习,我们了解了如何在 Tomcat 目录下手动部署 Servlet,这种方式不但效率低下,而且容易出错。因此,在实际开发中…

变量常量,基本数据类型及数据类型转换

⭐️ 变量常量与基本数据类型及数据类型转换 📍 来自:中南林业科技大学软件协会学术部:谢添 ⏲ 时间:2022 - 10 - 29 至 2022 - 10 - 30 🏠 官网:https://www.csuftsap.cn/ ✏️ 本章所有提供代码均已测…

四旋翼无人机学习第4节--STM32、MPU9250等器件的绘制

0 前言 当画stm32、mpu9250这种多引脚的芯片,就需要参考芯片手册啦。 这里给大家推荐一个芯片手册查询网站。 半导小芯-芯片查询工具 进入网站,输入芯片的具体名称,点击查询即可。 最后点击下载即可。 1 stm32芯片手册引脚查询 选择引脚…

学习在Git项目中使用子模块(图文教程)

一般认为 父项目 是当前正在做的主要工作,但需要依赖 子模块 中提供的算法或者工具。父项目 与 子模块 不是同一批人维护的,或者是需要分开维护的。 此情此景,需要学习该教程了!!! 文章目录1 如何在父项目…

基于Java的一个可自由拖拽的BI可视化系统(附源码)

介绍 这是一个可自由拖拽的BI可视化系统支持主流的关系数据:MySQL,Oracle,PostgreSQL等同时支持Apache Doris,这个一开始初衷就是为了 Doris 数据可视化分析做的后端框架使用了若依 功能 按项目管理数据看板看板具备分享功能可以…

每天五分钟机器学习:超平面分离定理和凸优化

凸集和凸函数 在点集拓扑学与欧几里得空间中,凸集是一个点集,其中每两点之间的直线上的点都落在该点集中。如下所示: 函数任意两点(x,f(x))和(y,f(y))连线上的值大于(x,y)区间内任意一点m的值f(m),那么这个函数就是一个凸函数: 超平面分离定理 空间中存在两类样本,…

【CV】第 3 章:使用 OpenCV 和 CNN 进行面部检测

🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎 📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃 🎁欢迎各位→点赞…

JavaScript语法知识笔记(一)——书写方式,输入出语句,变量,字面量,标识符,数据类型。

01.JS的三种书写方式 <!-- 2.内嵌式的js --><script>// alert(sajmo);</script><!-- 3.外部js script 双标签 --><script src"script.js"></script> <!-- JS代码需要写到script标签中 --><script type"text/jav…

软考高级-系统架构师-案例分析-架构设计真题考点汇总

2010年-2021年(不包括2019年和2020年)涉及到架构设计考点的有: 2010年题1,4; 2011年题1,4; 2012年题1; 2013年题1,4; 2014年题1,4; 2015年题1; 2016年题1; 2017年-题1; 2018年题1,5; 2021年题1 1.软件架构风格 软件架构风格是描述特定软件系统组织方式和惯用模式。组织方式描述…

2022第二届中国高校大数据竞赛A题(更新完毕)

文章目录题目任务做题解析第一问第三问第四问第一个预测第二个预测第五问关键技术摘要代码文件下载题目 制造业是国民经济的主体&#xff0c;近十年来&#xff0c;嫦娥探月、祝融探火、北斗组网&#xff0c;一大批重大标志性创新成果引领中国制造业不断攀上新高度。作为制造业…