【数据结构】单双链表超详解!(图解+源码)

news2024/11/28 19:43:29

在这里插入图片描述

🎥 屿小夏 : 个人主页
🔥个人专栏 : 数据结构解析
🌄 莫道桑榆晚,为霞尚满天!

文章目录

  • 📑前言
  • 🌤️链表概念
  • 🌤️链表的分类
    • ☁️单向或双向链表
    • ☁️带头或不带头
    • ☁️循环或不循环
    • ☁️常用的链表
  • 🌤️无头单向循环链表(单链表)
    • ☁️单链表的定义
    • ☁️结点
    • ☁️头插
    • ☁️尾插
    • ☁️查找
    • ☁️pos后一位插入
    • ☁️删除pos后一位
    • ☁️删除pos位置的值
    • ☁️打印
    • ☁️链表的释放
  • 🌤️带头双向循环链表
    • ☁️带头双向链表简介
    • ☁️链表主体
    • ☁️链表头部结点
    • ☁️添加新结点
    • ☁️链表打印
    • ☁️头插
    • ☁️尾插
    • ☁️头删
    • ☁️尾删
    • ☁️查找
    • ☁️pos位置前插入
    • ☁️删除pos位置结点
    • ☁️链表销毁
  • 🌤️链表优缺点总结
    • ☁️优点
    • ☁️缺点
  • 🌤️全篇总结

📑前言

什么是链表?链表有着什么样的结构性?它是怎么实现的?

看完这篇文章,你对链表的理解将会上升新的高度!

🌤️链表概念

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。下面是简单的单链表图。

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

🌤️链表的分类

链表的结构是多样的,以下的情况组合起来就有8种链表结构!

☁️单向或双向链表

在这里插入图片描述

☁️带头或不带头

在这里插入图片描述

☁️循环或不循环

在这里插入图片描述

☁️常用的链表

在这里插入图片描述

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单。

🌤️无头单向循环链表(单链表)

☁️单链表的定义

对类型进行重命名,这样以后可以根据自己的实际需求改变数据的类型。

创建一个结构体类型,存储元素数据,然后需要一个同样类型的结构体指针,这个指针可以指向同类型的数据,这样就可以通过指针访问下一个结点的元素。

typedef int SLDatatype;
 
typedef struct SListNode
{
	SLDatatype data;
	struct SListNode* next;
}SListNode;

链表这里定义后可不做初始化。

☁️结点

SListNode* BuySListNode(SLDatatype x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		perror("BuySListNode");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

在堆区上开辟一个新的结点,给定结点的值,然后初始化,节点是链表较为重要的一部分,没有结点,链表就无法链接。

☁️头插

void SLPushFront(SListNode** phead,SLDatatype x)
{
	SListNode* newnode = BuySListNode(x);
	newnode->next = *phead;
	*phead = newnode;
}

在链表前插入新结点,然后链接到原链表。

☁️尾插

void SLPushBack(SListNode** phead, SLDatatype x)
{
	SListNode* newnode = BuySListNode(x);
	if (*phead == NULL)
	{
		*phead = newnode;
	}
	else
	{
		SListNode* tail = *phead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

在链表末尾插入新结点,使原链表链接到新结点。

☁️查找

SListNode* SListFind(SListNode* phead, SLDatatype x)
{
	assert(phead);
	SListNode* pos = phead;
	while (pos)
	{
		if (pos->data == x)
			return pos;
		pos = pos->next;
	}
	return NULL;
}

给定要查找的链表中的元素,让pos去遍历,找到并返回当前元素的地址。

☁️pos后一位插入

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

通过查找找到要插入的位置,在指定位置的后一位插入元素数据。

☁️删除pos后一位

void SListEraseAfter(SListNode* phead)
{
	assert(phead);
	assert((phead)->next);
 
	SListNode* cur = phead->next;
	phead->next = cur->next;
	free(cur);
	cur = NULL;
}

通过查找找到要删除的位置,删除指定位置后一位的元素数据。

☁️删除pos位置的值

void SListErase(SListNode** phead, SListNode* pos)
{
	assert(pos);
	if (pos == *phead)
	{
		SLPopFront(phead);
	}
	else
	{
		SListNode* cur = *phead;
		while(cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = pos->next;
		free(pos);
		//pos = NULL;
	}
}

还给定链表的某个结点位置,然后删除,这里的pos可以置空也可以不置空,因为这是临时变量,出了函数就销毁了,好的习惯是可以置空的。

☁️打印

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

链表不像是数组,不能使用常规的方式来遍历。

☁️链表的释放

当链表不再使用后,我们要对其进行销毁,释放空间内存。

void SListDestroy(SListNode* phead)
{
	SListNode* current = phead;
	SListNode* next = NULL;
 
	while (current != NULL)
	{
		next = current->next;
		free(current);
		current = next;
	}
}
  1. 创建两个指针变量current和next,分别指向当前节点和下一个节点。
  2. 进入一个循环,循环条件是当前节点current不为NULL。
  3. 在循环内部,将next指针指向当前节点的下一个节点。
  4. 使用free函数释放当前节点的内存空间。
  5. 将current指针指向next,即将下一个节点赋给current,以便继续循环操作。
  6. 重复步骤3-5,直到链表中的所有节点都被释放掉。

🌤️带头双向循环链表

☁️带头双向链表简介

双向链表的节点通常包含两个部分:数据部分和指针部分。数据部分用于存储节点所包含的数据,指针部分包含两个指针,一个指向前一个节点,一个指向后一个节点。

​ 双向链表的优点是可以在常数时间内在任意位置插入或删除节点,因为只需要修改相邻节点的指针即可。而在单向链表中,如果要在某个位置插入或删除节点,则需要遍历链表找到该位置的前一个节点。

​ 双向链表相对于单向链表也有一些缺点。首先,双向链表需要额外的指针来存储前一个节点的地址,因此占用的内存空间比单向链表更大。其次,双向链表在插入或删除节点时需要修改两个指针的值,而单向链表只需要修改一个指针的值,因此操作起来更复杂。

到这里,想必大家就对双向链表有了个大概的认识,告诉你个小秘密哦:其实双向链表的实现比单链表要简单上不少,只是在数据的结构上双向链表看起来不让人觉得简单,别怕都是纸老虎,往下看一步步手撕它。

☁️链表主体

在这里插入图片描述

☁️链表头部结点

在对链表一系列的操作之前,我们首要需要的就是头结点点,有了头结点后续数据的插入删除都会变得简单。

List* ListCreate()
{
	List* newnode = (List*)malloc(sizeof(List));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->prev = newnode;
	newnode->next = newnode;
	newnode->val = 0;
	return newnode;
}

因为是循环的双向链表,所以头结点初始化的时候,两个指针都是指向自己。
在这里插入图片描述

☁️添加新结点

在插入数据中,必不可少的就是结点的创建,然后再链接到表中。新新结点的前后指针均为空,不指向如何结点。

List* BuyListNode(ListDatatype x)
{
	List* newnode = (List*)malloc(sizeof(List));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->val = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}

☁️链表打印

当链表有了数据以后,为了直观的方便我们对数据增删查改的观察,打印就起到了作用。

void ListPrint(List* pead)
{
	assert(pead);
	List* cur = pead->next;
 
	printf("pead:");
	while (cur != pead)
	{
		printf("《=》%d", cur->val);
		cur = cur->next;
	}
	printf("\n");
}

☁️头插

void ListPushFront(List* pead, ListDatatype x)
{
	assert(pead);
	List* newnode = BuyListNode(x);
	
	List* cur =pead->next;
	
	pead->next = newnode;
	newnode->prev = pead;
	newnode->next = cur;
	cur->prev = newnode;
	//ListInsert(pead->next, x);这是在pos位置前插入数据,这里可进行复用,后面会有实现
}

在这里插入图片描述

新结点的前指针指向前一个节点,后指针指向后一个节点,就进行了链接。

☁️尾插

void ListPushBack(List* pead, ListDatatype x)
{
	assert(pead);
 
	List* newnode = BuyListNode(x);
	List* cur = pead->prev;
 
	pead->prev = newnode;
	newnode->next = pead;
	cur->next = newnode;
	newnode->prev = cur;
	//ListInsert(pead, x);这是在pos位置前插入数据,这里可进行复用,后面会有实现
}

在这里插入图片描述

在尾部插入和头插同理,需要改变的只有相邻节点间的指针。

☁️头删

void ListPopFront(List* pead)
{
	assert(pead);
	assert(pead->next !=pead);
	List* cur = pead->next;
	List* second = cur->next;
 
	free(cur);
	pead->next = second;
	second->prev = pead;
}

在这里插入图片描述

删除首节点,然后使头部指针指向后一个节点。

☁️尾删

void ListPopBack(List* pead)
{
	assert(pead);
	assert(pead->prev);
	List* cur = pead->prev;
	List* second = cur->prev;
 
	free(cur);
	second->next = pead;
	pead->prev = second;
}

在这里插入图片描述

删除尾节点,然后使头部指针与尾节点前的节点链接。

☁️查找

List* ListFind(List* pead, ListDatatype x)
{
	assert(pead);
 
	List* cur = pead->next;
	while (cur != pead)
	{
		if (cur->val == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

给定一个元素的数据,然后在链表中进行遍历,找到后返回元素地址 。

☁️pos位置前插入

void ListInsert(List* pos, ListDatatype x)
{
	assert(pos);
	List* cur = pos->prev;
	List* newnode = BuyListNode(x);
 
	newnode->prev = cur;
	cur->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
}

通过断言确保输入的位置指针非空。创建新节点,并将其插入到指定位置之前。

  • 将指定位置的前一个节点保存为cur。
  • 创建一个新节点newnode,并将其数据域初始化为x。
  • 将新节点的前驱指针指向cur。
  • 将cur的后继指针指向新节点。
  • 将新节点的后继指针指向指定位置。
  • 将指定位置的前驱指针指向新节点。

☁️删除pos位置结点

void ListErase(List* pos)
{
	assert(pos);
	List* cur = pos->prev;
	List* second = pos->next;
	
	cur->next = second;
	second->prev = cur;
	free(pos);
}

断言确保输入的位置指针非空。将指定位置的前一个节点的后继指针指向指定位置的后一个节点,将指定位置的后一个节点的前驱指针指向指定位置的前一个节点,最后释放指定位置的内存空间。

☁️链表销毁

当链表我们不再需要使用的时候,就需要将其进行销毁,因为这些空间都是在堆上进行开辟的。

void ListDestroy(List* head) 
{
	assert(head);
 
	List* cur = head->next;
	while (cur != head) 
	{
		List* tmp = cur;
		cur = cur->next;
		free(tmp);
	}
	free(head);
}

🌤️链表优缺点总结

☁️优点

  1. 动态性:链表的长度可以根据需要进行动态调整,可以方便地进行插入和删除操作,而不需要像数组那样需要预先分配固定大小的内存空间。
  2. 灵活性:链表可以存储不同类型的数据,节点之间的连接关系可以根据需要进行调整,可以实现各种复杂的数据结构。
  3. 内存利用率高:链表只在需要时分配内存空间,不会浪费额外的内存。

☁️缺点

  1. 随机访问的效率低:链表中的元素并不是连续存储的,要访问链表中的某个元素,需要从头节点开始遍历,直到找到目标节点,因此访问某个特定位置的元素的时间复杂度为O(n),而不是O(1)。
  2. 额外的内存开销:链表中每个节点除了存储数据外,还需要额外的指针来连接节点,这会占用额外的内存空间。
  3. 不支持随机访问:由于访问链表中的元素需要遍历,因此无法像数组那样通过索引直接访问某个元素,这在某些应用场景下可能会造成不便。

🌤️全篇总结

本文对两种最常用到的链表形式进行了讲解,多方面由浅入深,让你真正掌握链表!

☁️ 后序还会有更多的数据结构文章分享哦!
看到这里希望给博主留个: 👍点赞🌟收藏⭐️关注!
你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。

在这里插入图片描述

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

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

相关文章

如何规范嵌入式软件开发

键盘的诱惑一直是所有太多嵌入式开发的失败。编写代码很有趣。很好 我们觉得我们正在该项目上取得进展。我们的老板通常不擅长构建固件的细微差别,他们赞成批准,微笑着,因为我们显然正在做有价值的事情。 作为从事基于汇编语言的系统的年轻开…

克隆音-自用教程

硬件准备: 8g以上显存的显卡(3060Ti以上)、16g以上内存、cpu是x86_64架构且支持avx2指令集、电源500w以上、1T的磁盘 free -g看内存 cat /proc/cpuinfo | grep avx2查指令集 资源准备 磁盘扩容 我扩大根目录 sudo lvextend -l 100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv …

坏死性凋亡+预后模型+实验,简单思路也能拿下7+。可升级

今天给同学们分享一篇坏死性凋亡预后模型实验的生信文章“Analysis of necroptosis-related prognostic genes and immune infiltration in idiopathic pulmonary fibrosis”,这篇文章于2023年3月27日发表在Front Immunol期刊上,影响因子为7.3。 IPF是一…

Vulkan Buffer 的构造的坑

Vulkan Buffer 的构造的坑 1. DeviceSize 和 memcpy 的大小是数组的数据总量的大小&#xff0c;而不是数组的元素个数 假设你读取模型之后的顶点和索引数组是这样的 std::vector<float> vertices; std::vector<uint16_t> indices;那么你传给 DeviceSize 和 m…

PerfectPixel 插件,前端页面显示优化工具

1.简介 PerfectPixel 插件是一款适用于 Chrome 浏览器的网页前端页面显示优化工具&#xff0c;该插件能够帮助开发人员和标记设计人员在开发时将设计图直接加载至网页中&#xff0c;与已成型的网页进行重叠对比&#xff0c;以规范网页像素精度 作为一款可以优化前端页面显示的…

AI AIgents时代-(六.)OpenAgents

最近Agents框架层出不穷&#xff0c;我们这次选择了OpenAgents&#xff0c;一个声称在Interface和Environment上全方面超越AutoGPT&#xff0c;OpenInterPreter等框架&#x1f92f; 接下来我们逐步拆解OpenAgents的独特之处&#xff01;OpenAgents开发的LLM-powered代理XLang集…

界面组件DevExtreme v23.1 —— UI模板库更新新功能

在DevExtreme在v22.2版本中附带了针对Angular、React和Vue的新UI模板库&#xff0c;这个新的UI模板库包含多个响应式UI模板&#xff0c;您可以将其用作业务应用程序的起点&#xff0c;模板包括类似CRM的布局、仪表盘、身份验证表单等。在这篇文章中&#xff0c;我们将看看在v23…

如何使用NXP RTD技术来完成AUTOSAR与NON-AUTOSAR的结合--以S32K3系列为例

目录 1、基本介绍 2、准备工作 3、从Can Demo开始 3.1 ASR CAN demo 3.1.1 文件概述 3.1.2 配置说明 3.1.3 文件结构 3.2 Non-ASR can通信 4 总结 1、基本介绍 RTD(Real Time Drivers)是NXP实现的一种复杂软件接口抽象&#xff0c;提供给符合AUTOSAR和非AUTOSAR的产品…

140CPU67260 5136-RE-VME 简化与外部分析软件平台的连接

140CPU67260 5136-RE-VME 简化与外部分析软件平台的连接 2022年5月26日-爱默生全球软件、技术和工程领导者今天宣布发布其PACSystems RSTi-EP CPE 200可编程自动化控制器(PAC)。这一新的紧凑型PACs系列通过最大限度地减少对专业软件工程人才的需求&#xff0c;帮助原始设备制…

如何让 Bean 深度感知 Spring 容器

Spring 有一个特点&#xff0c;就是创建出来的 Bean 对容器是无感的&#xff0c;一个 Bean 是怎么样被容器从一个 Class 整成一个 Bean 的&#xff0c;对于 Bean 本身来说是不知道的&#xff0c;当然也不需要知道&#xff0c;也就是 Bean 对容器的存在是无感的。 但是有时候我…

mac matplotlib显示中文

以下默认字体&#xff0c;在mac ventura上测试能成功显示中文&#xff1a; import matplotlib.pyplot as plt import matplotlib#from matplotlib import font_manager #plt.rcParams[font.sans-serif] [Heiti TC]#plt.rcParams[font.sans-serif] [Songti SC]#plt.rcParams[f…

goquery库编写程序

goquery库的爬虫程序&#xff0c;该程序使用Go来爬取视频。。 package main ​ import ("fmt""net/http""net/http/httputil""io/ioutil""log""strings""golang.org/x/net/proxy""golang.org/x/n…

C++失传千年经典系列(二):类

C失传千年经典系列(一):基础语法认知 忙着去耍帅,后期补充完整..............

Clion 下载、安装、使用教程,附详细图文(2023年亲测可用)

文章目录 一、下载Clion二、安装教程三、安装MinGW方法一、直接下载MinGW安装① 下载MinGW② 配置Clion 方法二、使用Dev cpp安装① 安装Dev cpp② 配置Clion 四、常用快捷键 大家好&#xff0c;今天为大家带来的是 Clion 的下载&#xff0c;安装&#xff0c;使用教程&#xff…

3.vue3项目(三):路由配置,登录页面搭建:登录功能调用,登录后的消息提示,登录时的表单校验

一、模板的路由的配置 首先我们需要登录页,首页,404页面,任意路由。 1.安装依赖 pnpm install vue-router 2.新建三个页面 新建登录页面、首页、404页面。 在src下面新建views文件夹,然后分别新建login,home,404三个文件夹,然后每个文件夹内新建一个index.vue。我们这…

社区智能奶柜,未来市场新机遇

我们无法左右大局&#xff0c;但可以通过对时代趋势的深入理解&#xff0c;精准把握机遇&#xff0c;乘势而上&#xff01;未来优秀的商业项目&#xff0c;将遵循以下几个标准&#xff1a;产品具有高频需求、刚性需求、高毛利空间和低人力成本。社区智能奶柜之所以能在当前市场…

【入门Flink】- 02Flink经典案例-WordCount

WordCount 需求&#xff1a;统计一段文字中&#xff0c;每个单词出现的频次 添加依赖 <properties><flink.version>1.17.0</flink.version></properties><dependencies><dependency><groupId>org.apache.flink</groupId><…

enum和Collection.stream()你这样用过么

最近在做一个数据图表展示的功能&#xff0c;显示订单近七天或者近半月的数量和金额。可以理解成下图所示的样子&#xff1a; 我是用枚举和集合的stream方法实现的数据初始化和组装&#xff0c;枚举用来动态初始化时间范围&#xff0c;集合的stream方法来将初始化的数据转换成…

《自制编程语言基于c语言》读书笔记

前言&#xff1a; 很久之前&#xff0c;我在双十一的时候入手了一本《自制编程语言基于c语言》。这本书是写《操作系统真象还原》的作者。我当时看他的关于操作系统的这本书&#xff0c;非常不错&#xff0c;就连着这本书一起入了。但是后面&#xff0c;因为各种事情&#xff…

龙芯浏览器是哪家公司开发的?支持信创吗?

最近看到不少小伙伴在问&#xff0c;龙芯浏览器是哪家公司开发的&#xff1f;支持信创吗&#xff1f;这里我们小编就跟大家一起来看看&#xff0c;仅供参考哈&#xff01; 龙芯浏览器是哪家公司开发的&#xff1f; 龙芯浏览器是由龙芯中科牵头&#xff0c;基于主流的渲染引擎G…