【数据结构初阶】 --- 单链表

news2024/11/16 11:43:40

关于链表你应该先了解这些

下图描述了物理模型和逻辑模型,大多数常见的其实是逻辑模型,但这对初学者或者掌握不扎实的同学不太友好,所以这里我重点讲解物理模型,当了解了这些细节,以后做题或是什么就直接画逻辑模型就好了
在这里插入图片描述
物理模型:
在这里,一个大方框表示着一个结点,这个结点里存储了两种数据:

  • 一是你本身要存储的数据
  • 二是为了让这些结点具有连接作用,而需要存放相应结点的地址,这样,当你访问了当前的结点内容,想要访问下一个结点的内容,这时就需要有下一个节点的地址,这就是为什么要存地址的原因。

链表的类型

单链表

上面的就是单链表

双链表

在这里插入图片描述
与单链表的不同就是一个结点里存有两个地址,可以实现双向访问,弥补了单链表只能单向访问的缺点

循环链表

在这里插入图片描述
单链表的最后一个结点存的是NULL,而循环链表的最后一个结点存放的是第一个结点的地址,事实上,你看这张图,确实也分辨不出哪个是第一个结点,所以想象成单链表最后一个结点放的头结点的地址就行

链表的存储方式

数组是一块连续的内存,而链表不同,每个结点都有自己的地址,并没有联系,这些地址是取决于操作系统的内存管理(在没学习操作系统之前可以当做这些地址是随机分配的)

链表的定义

这一节,我所讲的知识都是基于单链表

typedef int SLTDataType;//结点中存储数据的类型
typedef struct SListNode
{
	SLTDataType data;//结点中要存储的数据
	struct SListNode* next;//结点中指向下一个结点的指针
}SLTNode;

先看这张图:
在这里插入图片描述
与开头的那张区别就在于头指针phead,这是一个指针变量,用来存放第一个结点的地址,利用该指针就可以依次访问或操作节点中的数据
接下来初始化一个指向结点的头指针:(这里是头指针,不是头结点)

SLTNode* phead = NULL;

链表的操作

先了解是如何访问链表的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
完整流程:
在这里插入图片描述

为什么要用二级指针?

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

经过了头插操作,我们将new_node成功连入链表的头部,pphead存的也是new_node的地址,但是,我们的实参phead,它里面的内容竟还是之前第一个结点的内容,而往后我们还是要用phead来访问操作这个链表但新插入的节点却永远访问不到,那你说能不能用pphead访问不就行了,记住,pphead是局部变量,执行完头插这个函数pphead就不存在了,我们想操作这个链表,一直都是利用phead当做实参传递的。想要改变实参的内容只需传址调用即可,因此,我们将指针变量phead的地址当做实参传递过去,那么相应的,形参就需要一个二级指针接收,因为phead是指针,我们传递的是指针的地址,所以需要二级指针接收。如此,通过对二级指针pphead解引用就可以直接对phead的内容进行修改,到此,phead就可以存new_node地址了

创建一个新节点

SLTNode* SLTCreatNode(SLTDataType x)
{
//里用malloc函数向栈区开辟一个大小为sizeof(SLTNode)个字节的空间,将这个空间的首地址存放于new_p这个指针变量中
	SLTNode* new_p = (SLTNode*)malloc(sizeof(SLTNode));
	malloc函数开辟空间失败会返回NULL,这时就不要再进行后续操作,避免解引用空指针
	if (new_p == NULL)
	{
		perror("malloc");//进行报错,在屏幕上会出现错误提示
		exit(0);//直接让程序结束
	}
	//开辟成功就将x存入新的节点中,节点中的指针next指向NULL
	new_p->data = x;
	new_p->next = NULL;
	return new_p;
}

打印链表

这里只需将头指针的内容作为实参穿过来,利用形参指针phead接收就能达到效果

void SLTPrint(SLTNode* phead)//这里接收的是第一个结点的地址
{
	while (phead != NULL)
	{
		printf("%d->", phead->data);
		phead = phead->next;
	}
	printf("NULL\n");
}

头插法

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead != NULL);

	SLTNode* p_node = SLTCreatNode(x);
	p_node->next = *pphead;
	*pphead = p_node;
}

尾插法

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	//pphead里存的是phead的地址,不可能为NULL,所以如果传来NULL就会出问题,因此需要防止误传NULL指针,这里就要断言一下
	assert(pphead != NULL);

	SLTNode* p_node = SLTCreatNode(x);

	//试着将空链表的逻辑带入else中,你会发现cur->next在访问空指针,这是不行的,所以要专门为链表为空这个特殊情况写一段代码
	if (*pphead == NULL)
	{
		*pphead = p_node;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next != NULL)
		{
			cur = cur->next;
		}
		cur->next = p_node;
	}
}

头删法

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead != NULL);
	assert(*pphead != NULL);//如果传来的是个空链表就报错

	SLTNode* tmp = *pphead;
	*pphead = (*pphead)->next;
	free(tmp);
}

尾删法

//尾删的时候分三种情况讨论,没有节点,一个节点,多个节点
//不是说上来你就知道要分三种情况,而是当你写出适应多个节点的代码后
//这时你就要考虑边界情况,没有节点或者一个还是两个节点的情况
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead != NULL);
	assert(*pphead != NULL);//头指针为空(没有节点)就报错

	if ((*pphead)->next == NULL)//只有一个节点的情况
	{
		free(*pphead);
		*pphead = NULL;
	}
	else//多个节点
	{
		SLTNode* cur = *pphead;
		SLTNode* prev = NULL;
		while (cur->next != NULL)
		{
			prev = cur;
			cur = cur->next;
		}
		prev->next = NULL;
		free(cur);
	}
}

给指定的地址插入数据

这里是要将数据插入到pos前,为什么我们会知道一个结点的地址pos呢,答案就是查找函数帮你找的

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead != NULL);
	assert(pos != NULL);
	assert(*pphead);//传过来头指针指向的是空

	SLTNode* p_new = SLTCreatNode(x);

	if (*pphead == pos)
	{
		p_new->next = *pphead;
		*pphead = p_new;
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		p_new->next = prev->next;
		prev->next = p_new;
	

查找数据

返回的是查找到结点的地址

SLTNode* SLTFind(SLTNode* phead,SLTDataType x)
{
	assert(phead != NULL);

	SLTNode* cur = phead;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

销毁链表(意外的知识)

在C语言和C++中程序员在堆区开辟的数据需要程序员亲手释放,一个一个开出的节点,只能一个一个释放掉。
这里既可以用一级指针也可以用二级指针,不过有个小小的区别,当你用free释放一个指针的动态内存后需要及时置空,那么在这里,我们的头指针phead在释放完链表后也需要及时置空
但是,

  • 如果你传的实参是phead本身,那当SLTDestory执行完后,你需要额外的将phead手动置空
  • 如果传的是phead的地址,那么在函数SLTDestory中,释放完链表后,就可以操作*pphead = NULL,其实就等于在函数内部将函数外部的phead置空,出了函数,就不需要手动置空了
  • 到这里或许是有些抽象,你可以想一下free函数,它把指针释放后,需要你手动置空,实际上原因就是你传的是指针本身,free函数不能在它的内部将这个指针置空,所以需要你在free执行完手动置空
void SLTDestory(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		SLTNode* nxt = cur->next;
		free(cur);
		cur = nxt;
	}
}
void test()
{
	SLTNode* phead = NULL;
	...
	SLTDestory(phead);
	phead = NULL;//手动置空
}
void SLTDestory(SLTNode** pphead)//用二级指针接收phead的地址
{
	SLTNode* cur = *pphead;
	while(cur != NULL)
	{
		SLTNode* nxt = cur->next;
		free(cur);
		cur = nxt;
	}
	*phead = NULL;//远程操作置空
}
void test()
{
	LSTNode* phead = NULL;
	...
	SLTDestory(&phead);//实参是指针变量phead的地址
}

哨兵位

在这里插入图片描述

  • 哨兵位也算是一个结点,但哨兵位是不计入链表的长度。
  • 哨兵位里不需要存值,最好不要存,有的数据结构书上会将哨兵位存入链表的结点个数,但这里的前提是你创建的链表是存储int型的数据,如果是char呢,那么根据数据在内存中的存储方式不同,char型只能存储8个bit的数据,范围也就是-128~127,如果这个链表结点个数大于127,那么这个哨兵位存的数据就会出错,如果换做是浮点型,那就更离谱了,所以哨兵位最好不要存储数据。

初始化

LSTNode* phead = (LSTNode*)malloc(sizeof(LSTNode));
//phead这个指针变量指向哨兵位

传参

有了哨兵位,再也不用为二级指针苦恼了
每当我们可能对链表的头结点进行插入或者删除(实际是phead指向的结点的地址改变了,phead的内容需要更改,只能通过二级指针的方式远程操控phead)
现在哨兵位出现了,你想对链表的头结点进行操作,想改变头结点的地址,随便改,我phead现在指向是哨兵位,哨兵位的地址是固定不变的,再也不用担心要修改链表的同时还要考虑需不需要修改phead的指向,当哨兵位不改变时,链表怎样的增删改,都只会影响哨兵位的指向,与phead无关

这里用头删演示:

void SLTPopFront(SLTNode* phead)
{
	assert(phead != NULL);
	assert(phead->next != NULL);//当链表为空还要删就报错

	SLTNode* tail = phead->next;
	phead->next = tail->next;
	free(tail);
}

为什么都是用头删,头插演示呢,

  • 那是因为这两个操作更改的是第一个结点,节点发生改变,那就要更新指向这个结点的指针,现在的函数实参传的是哨兵位的地址,phead指向的也就是哨兵位,传的是哨兵位的地址那就可以对哨兵位进行更新,而远在函数外的头指针,一直指向的都是固定不变的哨兵位的地址。
  • 而前面我们如果更改了第一个结点,那就要更新头指针,想在函数里更改函数外的指针只能传指针的地址,指针的地址就需要二级指针接收。
  • 当然,重点讲带头单链表的原因也在于算法题里的链表大部分都是带头链表,避免与哨兵位混淆。哨兵位在某些时刻也会帮忙简化一些判断甚至帮助解题,后续有机会讲算法题会提到的。

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

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

相关文章

第8章 函数

第8章 函数 8.1 定义函数8.1.1 向函数传递信息8.1.2 实参和形参 8.2 传递实参8.2.1 位置实参8.2.2 关键字实参8.2.3 默认值 8.3 返回值8.3.1 返回简单值8.3.2 让实参变成可选的8.3.3 返回字典8.3.4 结合使用函数和 while 循环 8.4 传递列表8.4.1 在函数中修改列表8.4.2 禁止函数…

vue操作蓝牙教程

项目背景 想在VUE中使用蓝牙功能,百度了好久也尝试了好多都没法实现。 概念讲价 如果要在浏览器中使用蓝牙,去搜索关键字【navigator.bluetooth】,搜索后发现这根本不是想要的结果。 解决方法 去搜索关键字【uniappbluetoothvue】&#x…

mouceMice智能垃圾分类系统

mouceMice智能垃圾分类系统 1.成员名称和任务分配 成员认领任务陈曦实现登录、注册、检索垃圾类型和前端部分实现、连接树莓派实现socket通信传输图片杨雨佳需求分析和总体进展监督郑博文部分前端页面实现及其优化李睿初步实现深度学习算法、树莓派连接和算法效率提升范兴宇项…

【AI法官】人工智能判官在线判案?

概述 AI法官是一款为用户提供专业法律分析和判决建议的智能体应用。用户只需简要描述案情,AI法官便会利用其强大的法律知识和逻辑推理能力,快速且准确地梳理出判决结果。该应用的目标是为用户提供高效、准确、合法的判决建议。 角色任务 任务描述 作为…

【CS.SE】2024年,你应该选择计算机专业吗?详细分析与未来展望

文章目录 1. 引言1.1 背景介绍 2. 计算机相关专业的现状与挑战2. 计算机相关专业的现状与挑战2.1 行业内的就业趋势2.1.1 现有就业数据2.1.2 行业需求变化 2.2 市场饱和度与竞争2.2.1 毕业生数量增长2.2.2 薪资与职业发展 2.3 技术创新与行业发展2.3.1 新兴技术的发展2.3.2 全球…

TinyHttpd源码精读(三)

在上一章中我们一起看了如何实现静态的网页,在这里我们一起看Tinyhttpd最后的一部分,动态网页的实现:在这里首先声明下因为cgi脚本的支持问题,所以我会新建一个简单的cgi脚本然后将路径导向到这个脚本: 0.perl的配置&…

2024年建筑、水利交通与工程管理国际学术会议(ICAWRTEM 2024)

全称:2024年建筑、水利交通与工程管理国际学术会议(ICAWRTEM 2024) 会议网址:http://www.icawrtem.com会议地点: 广州投稿邮箱:icawrtemsub-conf.com 投稿标题:ICAWRTEM 2024ArticleTEL。投稿时请在邮件正文备注&#…

MySQL之高级特性(一)

高级特性 外键约束 InnoDB是目前MySQL中唯一支持外键的内置存储引擎,所以如果需要外键支持那选择就不多了。使用外键是有成本的。比如外键通常都要求每次在修改数据时都要在另一张表中多执行一次查找操作。虽然InnoDB强制外键使用索引,但还是无法消除这…

一夜之间,苹果杀死无数AI工具创业公司!GPT-4o深度整合进苹果

就在刚刚,苹果发布会WWDC2024官宣了一系列AI相关的重磅升级。 由于这一波AI升级攒的太大了,苹果甚至索性创造了一个新的概念——苹果智能(Apple Intelligence)。 如果你认为 苹果智能 Siri升级,那你就大错特错了。 …

分层解耦

三层架构 controller:控制层,接收前端发送的请求,对请求进行处理,并响应数据, service:业务逻辑层,处理具体的业务逻辑。 dao:数据访问层(Data Access Object)(持久层),负责数据访问操作,包括数…

动态规划(多重背包问题+二进制优化)

引言 多重背包,相对于01背包来说,多重背包是每个物品会有相应的个数,最多可以选那么多个,因而对于朴素多重背包,需要在01背包的基础上,再加一层物品的循环 朴素多重背包例题 P2347 [NOIP1996 提高组] 砝…

【Affine / Perspective Transformation】

文章目录 仿射变换介绍仿射变换 python 实现——cv2.warpAffine透视变换透视变换 python 实现——cv2.warpPerspective牛刀小试各类变换的区别与联系仿射变换和单应性矩阵透视变换和单应性矩阵 仿射变换介绍 仿射变换(Affine Transformation)&#xff0…

【话题】评价GPT-4o:从革命性技术到未来挑战

大家好,我是全栈小5,欢迎阅读小5的系列文章,这是《话题》系列文章 目录 引言技术原理应用领域实际案例优势挑战局限性未来展望文章推荐 引言 在人工智能领域,自然语言处理(NLP)技术的进步一直是推动技术革…

odoo15升级odoo16遇到的问题及解决过程

odoo15升级odoo16遇到的问题 PyMuPDF 档案管理整理时,从15升级16出现如下错误: File "f:\od162306\dms\dmssp\models\shenqb.py", line 136, in doc_fj_pdf doc.SaveAs(ftem, FileFormat=17) # input_file.replace(".docx", ".pdf") F…

鸿蒙开发文件管理:【@ohos.environment (目录环境能力)】

目录环境能力 该模块提供环境目录能力,获取内存存储根目录、公共文件根目录的JS接口。 说明: 本模块首批接口从API version 8开始支持。后续版本的新增接口,采用上角标单独标记接口的起始版本。本模块接口为系统接口,三方应用不支…

Pythone 程序打包成 exe

1.安装pyinstaller # 安装 pip install pyinstaller # 查看版本 pyinstaller -v2.更新pyinstaller 版本 # 更新 pip install --upgrade pyinstaller # 查看版本 pyinstaller -v3.切换到 py文件所在目录 #切换到.py所在的目录 E: cd cd E:\x-svn_x-local\04PythoneProjects\A…

滴滴出行 大数据研发实习生【继任】

大数据研发实习生JD 职位描述 1、负责滴滴核心业务的数据建设,设计并打造适应滴滴一站式出行平台业务特点的数仓体系。 2、负责抽象核心业务流程,沉淀业务通用分析框架,开发数仓中间层和数据应用产品。 3、负责不断完善数据治理体系&#xff…

远程链接服务 ssh

① 指定用户身份登录 ssh root10.36.105.100 ssh jim10.36.105.100 ② 不登陆远程执行命令 ssh root10.36.105.100 ls /opt ③ 远程拷贝 scp -r // 拷贝目录 -p // 指定端口 将本地文件拷贝给远程主机 scp -r /opt/test1 10.36.105.100:/tmp/// 将本…

使用 ML.NET CLI 自动进行模型训练

ML.NET CLI 可为 .NET 开发人员自动生成模型。 若要单独使用 ML.NET API(不使用 ML.NET AutoML CLI),需要选择训练程序(针对特定任务的机器学习算法的实现),以及要应用到数据的数据转换集(特征工程)。 每个数据集的最佳管道各不相同,从所有选择中选择最佳算法增加了复…

轻兔推荐 —— NeatDownloadManager

via:轻兔推荐 - https://app.lighttools.net/ 简介 NeatDownloadManager简称NDM,跟IDM同样出名的网络下载器,安装对应的浏览器器扩展后,可接管浏览器下载 - 软件体积非常小,Windows版900KB,很难想象当今的…