【数据结构】一篇带你彻底玩转 链表

news2024/11/28 22:44:47

文章目录

  • 链表的概念及结构
  • 链表的分类
  • 链表接口的实现
    • 链表打印
    • 链表申请节点
    • 链表尾插
    • 链表头插
    • 链表尾删
    • 链表头删
    • 链表查找
    • 链表在指定位置之后插入数据
    • 链表删除指定位置之后的数据
    • 链表在指定位置之前插入数据
    • 链表删除指定位置之前的数据
    • 链表删除指定位置的数据
    • 链表的销毁

链表的概念及结构

链表是一种物理存储结构上非连续、非顺序的存储结构,数是据元素的逻辑顺序是通过链表中的指针链接次序实现的 。C语言结构体指针在这里得到了充分的利用,可以在节点中定义多种数据类型, 并可以根据需要进行增删查改功能.

对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。想象一下,最后一个结点,它的指针指向哪里?

最后一个,当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为“空” 用NULL表示

  • 单链表的存储结构
struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
};
  • 链表存储结构图
    在这里插入图片描述
    注意:
  • 图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
  • 现实中的结点一般都是从malloc堆上申请出来的
  • 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

在这里插入图片描述在这里插入图片描述在这里插入图片描述

  • 虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
    构的子结构
    在这里插入图片描述
  1. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了
    在这里插入图片描述
  • 该章节我们讲解的是无头单向循环链表

链表接口的实现

无头+单向+非循环链表增删查改实现

链表打印

  • 链表与顺序表不同,顺序表是通过下标来访问每个元素。而链表是通过结构体存储的指针指向下一个节点来遍历整个数据的.
void SListPrint(SListNode* plist)
{
	assert(plist); //断言 判断是否为空指针
	SListNode* tmp = plist; 
	while (tmp != NULL)  //遍历整个链表,直到tmp指针指向空
	{
		printf("%d-> ", tmp->data); //打印节点数据
		tmp = tmp->next;	//将指针指向下一个结点
	}
	printf("NULL\n");
}

链表申请节点

  • 链表添加一个节点数据时候,每次都要写一段代码,这样做是不是太繁琐了,我们可以封装一个函数来解决问题,每次添加一个节点时,将结点存放的数据置为需要存放的值,在将结构体存放节点的地址置为NULL, 需要增加节点时调用一下改函数即可。
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode)); //申请一个结构体节点
	
	//判断是否申请成功
	if (newnode == NULL)
		perror("malloc:\n");
	
	newnode->data = x;
	newnode->next = NULL;
	//申请成功返回节点
	return newnode;
}

链表尾插

  • 在单链表进行尾插时,我们需要遍历整个链表直到找到最后一个节点才可以进行尾插,但如果此时链表为空时,我们直接插入节点数据即可。

错误代码示例:
相信许多人会写出这样的代码在这里插入图片描述 错误原因:

tail最后找到NULL了,tail和newnode 都是一个临时变量,把newnode给了tail,tail存放的是newnode里面的内容,tail出了作用域就不存在了。把newnode给了tail 并不会链接上链表的节点。最后还会存在内存泄漏. 所有我们这里要使用结构体指针来链接节点,正确写法应该让上一节节点找下一个节点的地址,让tmp->next (next这些节点都是在堆上的,并不会销毁) 找尾 ,将结构体指针指向的尾结点(NULL)指向新结点

正确代码示例

// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	//申请一个节点
	SListNode* newnode = BuySListNode(x);
	
	//当链表还没有节点时为空时,我们直接插入数据即可.
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		//进行找尾进行尾插
		//找到指针指向的尾结点(NULL)指向新结点
		SListNode* tmp = *pplist;
		while (tmp->next != NULL)  
		{
			tmp = tmp->next;
		}
		tmp->next = newnode;//将尾结点中存放的指针置为插入结点的地址即可链接上
	}
}

链表头插

  • 头插逻辑相对简单.只需要申请一个节点,让节点的指针指向头指针,在把头指针指向申请节点的位置
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
	//申请一个节点
	SListNode* newnode = BuySListNode(x);
	
	newnode->next = *pplist;
	*pplist = newnode;
}

链表尾删

  • 尾删的细节较多一点
    1: 链表为空时,无须删除
    2: 链表只有一个元素时,做特需处理
    3: 正常处理

这里提供两种方法给大家参考

  • 方法一: 前后指针
    使用一个指向为空的指针 prev和一个指向头节点的指针cur,当cur->next指向的不是NULL时我们让prev 指向cur节点,而cur指向cur的下一个节点,循环直到cur->next指向null时,prev->next指向的就是我们要删除的最后一个节点.
void SListPopBack(SListNode** pplist)
{
	assert(*pplist);  //断言 判断是否为空指针
	
	//当只有一个节点时
	if ((*pplist)->next == NULL)
	{
	//只有一个结点,直接释放该结点,然后将结点置为NULL
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* cur = *pplist; 
		SListNode* prev = NULL;
		while (cur->next != NULL)
		{
			prev = cur;	
			cur = cur->next; //循环遍历
		}
		free(prev->next); //释放尾结点
		prev->next = NULL;
	}
}
  • 方法二: 知需判断cur->next->next是否为空,如果为空释放cur->next。当链表只有两个节点时循环不进去,直接释放cur->next.
void SListPopBack(SListNode** pplist)
{
	assert(*pplist);//断言 判断是否为空指针

	//只有一个结点,直接释放该结点,然后将结点置为NULL
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* cur = *pplist;
		while (cur->next->next != NULL)
		{
			cur = cur->next;//循环遍历
		}
		free(cur->next); //释放尾结点
		cur->next = NULL;
	}
}

链表头删

  • 链表头删逻辑也简单,只需要注意一下链表是否为空。然后用一个临时变量保存头指针的->next,然后在把头指针销毁,把头指针给回临时变量即可.
// 单链表头删
void SListPopFront(SListNode** pplist)
{
	assert(*pplist);  //断言 判断是否为空指针
	
	SListNode* tmp = (*pplist)->next;//保存头指针的下一个节点
	free(*pplist); //销毁释放头指针
	*pplist = tmp; 头指针指向释放前的下一个节点
}


链表查找

  • 获得链表某个节点的数据思路也较简单
    1: 定义一个cur指针指向头节点,不断指向下一个节点
    2: 如查找成功,返回节点cur的数据
    3: 如cur指向Null已经遍历完了,则说明查找的内容不存在,返回空指针。
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	assert(plist);//断言 判断是否为空指针
	SListNode* cur = plist;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

链表在指定位置之后插入数据

  • 先把新节点的next 指向pos->next, 然后再把pos->next 更新成新节点.
void SLInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);//断言 判断是否为空指针

	SListNode* newnode = BuySListNode(x);//申请一个节点
	newnode->next = pos->next; //新节点指向pos位置的下一个节点
	pos->next = newnode; //pos下一个位置插入新节点
}

链表删除指定位置之后的数据

  • 整体逻辑就是保存将要删除位置下一个节点的位置,把pos->next后的位置在链接上要删除位置的下一个节点。 如果pos下一个节点为空,无需删除直接返回即可.
void SListEraseAfter(SListNode* pos)
{
	assert(pos);//断言 判断是否为空指针
	
	if (pos->next == NULL) //如果pos下一个节点为空,无须删除
		return;
	SListNode* cur = pos->next->next;//保存要删除位置的下一个节点
	free(pos->next); //释放pos之后的数据
	pos->next = cur; //链接上要删除位置的下一个节点
}

链表在指定位置之前插入数据

  • 算法思路:
    1: 当指定位置为第一个元素时,这时我们只需调用一下头插函数即可.
    2: 遍历链表,找到pos上一个节点的数据.
    3: 找到上一个节点数据,把新节点的next指向指定位置,在把找到的节点指向新开辟的节点,这样就可以链接上了。
void SLInsertfront(SListNode** pplist, SListNode* pos, SLTDateType x)
{
	assert(pos);//断言 判断是否为空指针
	if (*pplist == pos) //当pos位置是第一个节点数据时,那就是头插了
	{
		SListPushFront(pplist, x);//头插
	}
	else
	{
		SListNode* tmp = *pplist;
		while (tmp->next != pos)
		{
			tmp = tmp->next;//循环遍历
		}
		SListNode* newnode = BuySListNode(x);
		newnode->next = pos; //把新节点的next指向指定位置
		tmp->next = newnode; //找到的节点指向新开辟的节点
	}
}

链表删除指定位置之前的数据

  • 整体思路:
  • 1: 当指定位置pos是第一个节点时,无需删除,直接返回即可。
  • 2: 当指定位置pos是第二个节点数据时,只需要进行头删即可。
  • 3: 遍历数组,找到pos 之前要删除节点数据的上一个节点。把该节点的next(就是pos上一个节点的数据)直接销毁释放掉,再把找到的节点next重新链接上pos.
void SLErasefront(SListNode** pplist, SListNode* pos)
{
	assert(pos); //断言 判断是否为空指针

	if (pos == *pplist) //当指定是第一个节点时,无需再删除,直接返回.
	{
		printf("pos位置前为空\n");
		return;
	}
	else if ((*pplist)->next == pos)//当指定位置pos是第二个节点数据时,只需要进行头删即可
	{
		SListPopFront(pplist);//头删
	}
	else
	{
		SListNode* tmp = *pplist;
		while (tmp->next->next != pos)
		{
			tmp = tmp->next; //遍历循环
		}
		free(tmp->next);//释放pos之前的节点数据
		tmp->next = pos; //链接上pos
	}
}

链表删除指定位置的数据

  • 思路:
    1: 当指定位置pos是第一节点数据时,直接使用头删即可。
    2: 查找指定位置pos上一个节点位置,再把该位置的next链接上pos的next位置.
    3: 释放指定位置的数据。
void SLErase(SListNode** pplist, SListNode* pos)
{
	assert(pos);//断言 判断是否为空指针
	
	if (pos == *pplist)//当pos是第一节点数据时,直接使用头删即可
	{
		SListPopFront(pplist);//头删
	}
	
	else
	{
		//
		SListNode* tmp = *pplist;
		while (tmp->next != pos)//找pos上一个节点
		{
			tmp = tmp->next;
		}
		tmp->next = pos->next;//将需要删除的结点的上一个结点的next指向需要删除的下一个结点
		free(pos);//释放指定位置的数据
		pos = NULL;
	}
}


链表的销毁

当我们不打算使用这个单链表时,我们需要把它销毁,其实也就是在内存中将它释放掉,以便于留出空间给其他程序或软件使用。

  • 思路:保存当前节点的下一个结点的地址,释放当前结点,再将指针指向刚刚保留的结点,如此循环直到为空。链表销毁成功.
// 单链表的销毁
void SListDestroy(SListNode* plist)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur != NULL)
	{
		SListNode* next = cur->next; //保存下一个节点
		free(cur); //释放当前指定
		cur = next; //指向下一个节点
	}
}

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

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

相关文章

总结835

学习目标: 4月(复习完高数18讲内容,背诵21篇短文,熟词僻义300词基础词) 学习内容: 暴力英语:熟练背诵《大独裁者》,最后默写。抄写今后要背诵的两篇文章。 高等数学:做…

机器视觉各开发语言对比以及选择

机器视觉主流开发语言主要有, 一.C#,占有率极高 市面主要以Halcon,visionpro,visionmaster,opencvsharp为主。 开发人员利用 C# 能够生成在 .NET 中运行的多种安全可靠的应用程序。 二.C++,Qt 市面主要以Halcon,visionpro,visionmaster,opencv为z主。 C++ 即已成为世界上…

Arduino学习笔记5

一.直流电机控制实验 1.源代码 int dianJiPin9;//定义数字9接口接电机驱动IN1的控制口void setup() {pinMode(dianJiPin,OUTPUT);//定义电机驱动IN1的控制口为输出接口 } void loop() {digitalWrite(dianJiPin,LOW);//关闭电机delay(1000);//延时digitalWrite(dianJiPin,HIGH…

基于protobuf构建grpc服务

一、protobuf介绍 protobuf是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为profobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。 优势: 序列…

【Unity-UGUI控件全面解析】| Text文本组件详解

🎬【Unity-UGUI控件全面解析】| Text文本组件详解一、组件介绍二、组件属性面板三、代码操作组件四、组件常用方法示例4.1 改变Text文本颜色4.2 文本换行问题4.3 空格自动换行问题4.4 逐字显示效果五、组件相关扩展使用5.1 文本描边组件(Outline)5.2 阴影组件(Shadow)5.3…

操作系统——操作系统逻辑结构

0.关注博主有更多知识 操作系统入门知识合集 目录 2.1操作系统的逻辑结构 思考题: 2.2CPU的态 思考题: 2.3中断机制 2.1操作系统的逻辑结构 操作系统的结构指的是操作系统的设计和实现思路,按照什么样的结构设计、实现。 操作系统的…

[java]云HIS:检验字典维护

术语解释: 1、最小剂量:并非指医生开处方时的最小剂量值,而是为了对应计量单位和剂量单位之间数量关系而设置的。 2、包装规格:是计价单位和计量单位之间换算的关系值,1个计价单位计价规格个计量单位。 药品单位之间的…

【三十天精通Vue 3】第二十一天 Vue 3的安全性详解

✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: 三十天精通 Vue 3 文章目录 引言一、Vue 3 中的安全问题1.1 前端安全问题概述1.2 Vue 3 中的安…

浅谈Golang等多种语言转数组成字符串

目录 Python 一维列表转字符串 二维列表转字符串 多维列表转字符串 Golang 一维数组的遍历打印 二维数组的遍历打印 Java 一维容器的直接打印 二维容器的直接打印 普通数组的转化 C 一维容器的遍历 1. to_string() 2. stringstream 二维容器的遍历 简要小结 …

【Python--高级教程】

高级教程 1.正则表达式re.compile()re.match()函数re.search()函数re.search()函数与re.match()函数的区别group(num) 或 groups()检索和替换re.sub()替换函数中的re.sub可以是一个函数findAll()方法re.finditer()方法re.split()regex修饰符正则表达式模式 2.CGI编程什么是CGI网…

Top-K问题

Top-K简介 😄Top-k算法常用于对诸如前几名,前几个最大值,前几个最小值这样的问题的求解,并且在数据量较大时力求在最短的时间内求出问题的解。例如: 世界500强公司,世界上年龄最大的几个人,某知…

3.7 Linux shell脚本编程(分支语句、循环语句)

目录 分支语句(对标C语言中的if) 多路分支语句(对标C语言中的swich case) 分支语句(对标C语言中的if) 语法结构: if 表达式 then 命令表 fi 如果表达式为真, 则执行命令表中的命令; 否则退出if语句,…

数据湖Data Lakehouse支持行级更改的策略:COW、MOR、Delete+Insert

COW:写时复制,MOR:读时合并,Delete+Insert:保证同一个主键下仅存在一条记录,将更新操作转换为Delete操作和Insert操作 COW和MOR的对比如下图,而Delete+Insert在StarRocks主键模型中用到。 目前COW、MOR在三大开源数据湖项目的使用情况,如下图。 写入时复制【Copy-On…

浙大的SAMTrack,自动分割和跟踪视频中的任何内容

Meta发布的SAM之后,Meta的Segment Anything模型(可以分割任何对象)体验过感觉很棒,既然能够在图片上面使用,那肯定能够在视频中应用,毕竟视频就是一帧一帧的图片的组合。 果不其然浙江大学就发布了这个SAMTrack,就是在…

编译预处理以及相关面试

编译预处理 1、宏定义1.1、 无参宏定义1.2、使用宏定义的优点1.3、宏定义注意点1.4、带参数的宏(重点)1.5、条件编译1.6、宏定义的一些巧妙用法(有用)1.7、结构体占用字节数的计算原则(考题经常考,要会画图)1.8、#在宏定义中的作用&#xff0…

[Android Studio Tool]如何将AS的gradle文件迁移到D盘

解决学习安卓的过程中,使用Android Studio来进行开发导致的C盘空间占用的问题。 首先,找到C盘中的.gradle文件的位置 一般会在我们的系统盘的用户文件下。 然后把一整个.gradle文件剪切,粘贴到其它盘(比如D盘)的根目录下 打开Androdi Stu…

QT菜单样式Ribbon Control for Qt, Office ribbon control

基于Qt(最低要求Qt5,支持C11的编译器)开发的一个轻量级的Ribbon控件(Office样式UI) 使用Qt Creator直接打开SARibbon.pro,并编译即可,会编译出SARibbonBar库和一个(目前只有一个例子)例子&#…

7.0、Java继承与多态 - 多态的特性

7.0、Java继承与多态 - 多态的特性 面向对象的三大特征:封装性、继承性、多态性; extends继承 或者 implements实现,是多态性的前提; 用学生类创建一个对象 - 小明,他是一个 学生(学生形态)&…

nginx(七十二)nginx中与cookie相关的细节探讨

背景知识铺垫 一 nginx中与cookie相关 ① Cookie请求头内容回顾 cookie的形式和属性 ② nginx获取cookie值的两种方法 1) $http_cookie -->获取Cookie请求头"所有值"2) $COOKIE_flag -->获取Cookie请求头的"某个key"[1]、脱敏场景在日志中只…

【操作系统复习】第6章 虚拟存储器 1

前面所介绍的各种存储器管理方式,有一个共同特点:作业全部装入内存后方能运行 问题: ➢ 大作业装不下 ➢ 少量作业得以运行 解决办法: ➢ 方法一:从物理上增加内存容量,成本高 ➢ 方法二:…