《单链表》的实现(不含哨兵位的单向链表)

news2024/11/25 4:21:00

目录

​编辑

前言:

 链表的概念及结构:

链表的实现:

1.typedef数据类型:

2.打印链表 :

3.创建新节点:

4.尾插 :

5.头插:

6.尾删 :

7.头删:

8.查找节点:

9.指定下标前插入:

10.删除当前下标 

11. 指定下标后插入:

12.删除当前下标的后一个节点 :

13.销毁链表:

总结:


前言:

我们在前面的学习中深度的讲解了顺序表的模拟实现,而在上一篇好题分享中,我们又对于链表中的几道基础题(含有含金量)作出了完善的解析,今天我们将要真正的开启链表的学习,就从最基础的模拟实现一个单链表开始

前两篇的blog在这里:

好题分析(2023.10.29——2023.11.04)-CSDN博客

《动态顺序表》的实现-CSDN博客

 链表的概念及结构:

链表(Linked List)是一种常见的数据结构,它是由一系列节点(Node)组成的,每个节点包含两个部分:数据域和指针域。数据域存储数据,指针域指向下一个节点。

链表可以分为单向链表和双向链表两种:

单向链表:每个节点只有一个指针域,指向下一个节点。

双向链表:每个节点有两个指针域,一个指向前一个节点,一个指向后一个节点。

链表相较于数组,具有以下优势:

  1. 内存空间可以动态分配,不需要预先定义大小。

  2. 插入和删除操作比较容易,只需要改变指针指向即可,不需要移动元素。

  3. 可以节省内存空间,因为链表中的节点可以零散分布在内存中,不需要连续的空间。

但是链表的缺点是:

  1. 访问元素的时间复杂度为O(n),而数组可以通过下标随机访问元素,时间复杂度为O(1)。

  2. 链表的节点需要额外的指针域来存储指向下一个节点的指针,因此内存占用相对于数组较大。

这些特点使得链表在某些场景下比数组更加适用,比如实现队列、栈、哈希表等数据结构,或者需要频繁插入和删除元素的场景。

介绍完链表的基本信息,下面我们就来实现链表!

链表的实现:

1.typedef数据类型:

typedef int SLNDataType;

typedef struct SlistNode
{
	SLNDataType val;
	struct SlistNode* next;
}SLNode;

这里与我们最早实现通讯录和顺序表的结构相似,在这里我就不过多赘述。

2.打印链表 :

void SLTPrint(SLNode* phead)
{
	while (phead)
	{
		printf("%d->", phead->val);
		phead = phead->next;
	}
	printf("NULL\n");
}

打印函数的内容较好理解,在这里我不进行过多的赘述。

3.创建新节点:

 

SLNode* CreateNode(SLNDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("CreateNode -> malloc");
		exit(-1);
	}
	newnode->next = NULL;
	newnode->val = x;
	return newnode;
}

这里与我们在顺序表中,在堆区申请空间而使用的malloc相似。

在这里我们是定义了一个指针newnode,这里要注意的是此时的newnode的返回值是SLNode*,意思就是指向一个SLNode类型的结构体指针! 

所以对于该结构体我们就给它赋予初始值,即newnode->val = x;newnode->next = NULL;

4.尾插 :

void SLTPushBack(SLNode** pphead, SLNDataType x)
{
	assert(pphead);
	SLNode* newnode = CreateNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;

	}
}

对于此函数的实现,我们首先要讲解的就是此时的形参必须是一个二级指针!

我们都知道,

形参是实参的一份临时拷贝!

 我们在实现这些关于结构体的函数,传递的都是结构体的地址。

对于顺序表和通讯录,我们这样传递不成问题。例:

Contact c;

CreateContact(&c);

等等代码。因为我们一开始是创建了一个结构体C,这时我们想要在结构体里修改各个成员的值,就需要调用CreateContact函数,那么我们都知道要传入结构体指针,因为当我们传入指针,指针可以在原来的数据上进行修改,同时可以返回到修改完的数据。

但当我们仅仅传一个C,而非指针时,此时函数内部就会再创建一个结构体,对新创建的结构体里的各个成员进行修改,一旦函数结束,此结构体就会销毁,原来的结构体不会发生改变。

这就是为什么形参是实参的一份临时拷贝!

同理,

对于当前的链表,我们在主函数肯定会有:

SLNode* plist = NULL;

 

我们在此刻是使用的结构体指针,而非结构体对象,因为我们使用结构体指针可以更方便访问链表的头结点,进而进行许许多多的操作。

那如果我们在函数里用的是一级指针的话,就会出现:

当我们实施添加节点时, 

 

看似我们创建了新节点,但实际上是这样的。

一旦函数结束,phead就会销毁,新开辟出来的结点就会找不到。

就会导致内存泄漏。

所以正确的做法就是利用二级指针,来解引用得到原指针而修改里面的next,并且指向新节点。

即:

 

 

 这就很好的解释了为什么要用双指针来接收。

然后就是要注意,当指针为NULL和指针已经指向节点要分开讨论。

5.头插:

void SLTPushFront(SLNode** pphead, SLNDataType x)
{
	assert(pphead);
	SLNode* newnode = CreateNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

头插的代码很好理解,在这里我不做过多赘述。

6.尾删 :

void SLTPopBack(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLNode* tail = *pphead;
	if (tail->next == NULL)
	{
		free(tail);
		tail = NULL;
	}
	else
	{
		SLNode* prev = NULL;
		while (tail->next)
		{
			prev = tail;
			tail = tail->next;
		}
		prev->next = NULL;
		free(tail);
		tail = NULL;
	}
	
}

对于该函数的实现,注意的就是要保存最后一个节点前的结点,这样我们就可以将前一个节点的next指向NULL。

同时注意要将空节点和有节点分开讨论。

7.头删:

void SLTPopFront(SLNode** pphead)
{
	assert(*pphead);
	assert(pphead);
	SLNode* tmp = (*pphead)->next;
	free(*pphead);
	*pphead = tmp;
}

头删的大致思路与尾删类似,在这里我不过多赘述。


8.查找节点:

SLNode* SLTFind(SLNode* phead, SLNDataType x)
{
	assert(phead);
	while (phead)
	{
		if (phead->val == x)
		{
			return phead;
		}
		phead = phead->next;
	}
	return NULL;
}

9.指定下标前插入:

void SLTInsertBefore(SLNode** pphead, SLNode* pos, SLNDataType x)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	
	SLNode* tail = *pphead;
	if ((*pphead) == NULL)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		while (tail->next != pos)
		{
			tail = tail->next;
		}
		SLNode* newnode = CreateNode(x);
		newnode->next = pos;
		tail->next = newnode;
	}
}

10.删除当前下标 

void SLTErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	SLNode* tmp = *pphead;

	if (*pphead == pos)
	{
		SLTPopBack(pphead);
	}

	else
	{
		while (tmp->next != pos)
		{
			tmp = tmp->next;
		}

		tmp->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

11. 指定下标后插入:

void SLTInsertAfter(SLNode* pos, SLNDataType x)
{
	assert(pos);
	SLNode* newnode = CreateNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

12.删除当前下标的后一个节点 :

 

void SLTEraseAfter(SLNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLNode* tmp = pos->next;
	pos->next = tmp->next;
	free(tmp);
	tmp = NULL;
}

13.销毁链表:

void SLTDestory(SLNode** pphead)
{
	assert(pphead);
	SLNode* prev = NULL;
	while (*pphead)
	{
		prev = *pphead;
		*pphead = (*pphead)->next;
		free(prev);
	}
	prev = NULL;
}

总结:

 在这里我仅仅只是讲解了实现单链表的基本概念,而对于后续的一些函数实现,我并没有进行过多的讲解,因为这些函数的实现与之前的尾插和头插大差不差。

需要注意的就是释放和插入的next指向,以及先后顺序。

这些在之后做题中会多次提到,我们也会不断熟练。

总之,对于这部分链表问题,我们应当多多练习多多刷题,提升我们的熟练度才是首要目标!

记住

“坐而言不如起而行”

Action speake louder than words

以下是我本次文章的全部代码:

Data structures amd algorithms: 关于数据结构和算法的代码 - Gitee.com

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

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

相关文章

VScode + opencv(cmake编译) + c++ + win配置教程

1、下载opencv 2、下载CMake 3、下载MinGW 放到一个文件夹中 并解压另外两个文件 4、cmake编译opencv 新建文件夹mingw-build 双击cmake-gui 程序会开始自动生成Makefiles等文件配置,需要耐心等待一段时间。 简单总结下:finish->configuring …

041:vue中 el-table每个单元格包含多个数据项处理

第041个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下,本专栏提供行之有效的源代码示例和信息点介绍,做到灵活运用。 (1)提供vue2的一些基本操作:安装、引用,模板使…

USB偏好设置-Android13

USB偏好设置 1、USB偏好设置界面和入口2、USB功能设置2.1 USB功能对应模式2.2 点击设置2.3 广播监听刷新 3、日志开关3.1 Evet日志3.2 代码中日志开关3.3 关键日志 4、异常 1、USB偏好设置界面和入口 设置》已连接的设备》USB packages/apps/Settings/src/com/android/setting…

写一下关于部署项目到服务器的心得(以及遇到的难处)

首先要买个服务器(本人的是以下这个) 这里我买的是宝塔面板的,没有宝塔面板的也可以自行安装 点击登录会去到以下页面 在这个界面依次执行下面命令会看到账号和密码和宝塔面板内外网地址 sudo -s bt 14点击地址就可以跳转宝塔对应的内外网页面 然后使用上述命令提供的账号密…

听GPT 讲Rust源代码--library/core/src

题图来自 The first unofficial game jam for Rust lang![1] File: rust/library/core/src/hint.rs rust/library/core/src/hint.rs文件的作用是提供了一些用于提示编译器进行优化的函数。 在Rust中,编译器通常会根据代码的语义进行自动的优化,以提高程序…

LeetCode(6)轮转数组【数组/字符串】【中等】

目录 1.题目2.答案3.提交结果截图 链接: 189. 轮转数组 1.题目 给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1…

npm install 报错 chromedriver 安装失败的解决办法

npm install chromedriver --chromedriver_cdnurlhttp://cdn.npm.taobao.org/dist/chromedriver

Linux imu6ull驱动- led

一、GPIO模块结构 开始来啃手册了,打开我们的imx6ull手册。本章我们编写的是GPIO的,打开手册的第28章,这一章就有关于IMX6ULL 的 GPIO 模块结构。 mx6ull一共有5 组 GPIO(GPIO1~GPIO5) GPIO1 有 32 个引脚&…

Python 的 datetime 模块

目录 简介 一、date类 (一)date 类属性 (二)date 类方法 (三)实例属性 (四)实例的方法 二、time类 (一)time 类属性 (二)tim…

听GPT 讲Rust源代码--library/core/src(2)

题图来自 5 Ways Rust Programming Language Is Used[1] File: rust/library/core/src/iter/adapters/by_ref_sized.rs 在Rust的源代码中,rust/library/core/src/iter/adapters/by_ref_sized.rs 文件实现了 ByRefSized 适配器,该适配器用于创建一个可以以…

基于遗传算法优化的直流电机PID控制器设计

PID控制器是工业控制中常用的一种控制算法,通过不断调节比例、积分和微分部分来实现对系统的稳定控制。然而,在一些复杂系统中,传统的PID参数调节方法可能存在局限性。本文将介绍一种基于遗传算法优化的直流电机PID控制器设计方法&#xff0c…

AIGC:使用bert_vits2实现栩栩如生的个性化语音克隆

1 VITS2模型 1.1 摘要 单阶段文本到语音模型最近被积极研究,其结果优于两阶段管道系统。以往的单阶段模型虽然取得了较大的进展,但在间歇性非自然性、计算效率、对音素转换依赖性强等方面仍有改进的空间。本文提出VITS2,一种单阶段的文本到…

windows下安装es及logstash、kibna

1、安装包下载 elasticsearch https://www.elastic.co/cn/downloads/past-releases#elasticsearch kibana安装包地址: https://www.elastic.co/cn/downloads/past-releases/kibana-8-10-4 logstash安装包地址: https://www.elastic.co/cn/downloads/past…

自适应模糊PID控制器在热交换器温度控制中的应用

热交换器是一种常见的热能传递设备,广泛应用于各个工业领域。对热交换器温度进行有效控制具有重要意义,可以提高能源利用效率和产品质量。然而,受到热传导特性和外部环境变化等因素的影响,热交换器温度控制难度较大。本文提出一种…

带你走进Cflow (三)·控制符号类型分析

目录 ​编辑 1、控制符号类型 1.1 语法类 1.2 符号别名 1.3 GCC 初始化 1、控制符号类型 有人也许注意到了输出中奇怪的现象:函数_exit 丢失了,虽然它在源文件中被printdir 调用了两次。这是因为默认情况下 cflow 忽略所有的一下划线开头的符号…

离线视频ocr识别

sudo apt-get install libleptonica-dev libtesseract-dev sudo apt-get install tesseract-ocr-chi-sim python -m pip install video-ocrwindows安装方法: 下载安装 https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-w64-setup-5.3.3.20231005.exe 下…

python自动化测试selenium核心技术3种等待方式详解

这篇文章主要为大家介绍了python自动化测试selenium的核心技术三种等待方式示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪 UI自动化测试过程中,可能会出现因测试环境不稳定、网络慢等情况&a…

JavaEE初阶学习:JVM(八股文)

1.JVM 中的内存区域划分 JVM 其实是一个Java进程~ java 进程会从操作系统这里申请一大块内存区域,给java代码使用~ 内存区域进一步划分,给出不同的用途 1.堆 new 出来的对象 (成员变量) 2.栈 维护方法之间的调用关系 (局部变量) 3.方法区(旧) / 元数据区 (新) 放的是类加载之…

K8S容器持续Terminating无法正常关闭(sider-car容器异常,微服务容器正常)

问题 K8S上出现大量持续terminating的Pod,无法通过常规命令删除。需要编写脚本批量强制删除持续temminating的Pod:contribution-xxxxxxx。 解决 获取terminating状态的pod名称的命令: # 获取media命名空间下,名称带contributi…

【论文解读】针对生成任务的多模态图学习

一、简要介绍 多模态学习结合了多种数据模式,拓宽了模型可以利用的数据的类型和复杂性:例如,从纯文本到图像映射对。大多数多模态学习算法专注于建模来自两种模式的简单的一对一数据对,如图像-标题对,或音频文本对。然…