数据结构·单链表

news2024/12/23 5:10:58

        不可否认的是,前几节我们讲解的顺序表存在一下几点问题:

        1. 中间、头部的插入和删除,需要移动一整串数据,时间复杂度O(N)

        2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗

        3. 增容一般是2倍的增长,这势必会造成空间的浪费

        那如何解决这些问题呢,此时,链表出现了

1. 链表的概念和结构

        我们之前说过,线性表的特点就是逻辑上是连续的,物理上不一定连续。顺序表是逻辑上是连续的,物理上也是连续的。而今天的链表就是逻辑上是连续的,但是物理上是不连续的

        最简单的链表是由节点们串在一起组成的,每个节点包含了两个内容:

                1. 要存入的有效数据

                2. 下一个节点的地址

        可以看出,每个节点在物理上都是独立的,不连续的。但是每个节点在逻辑上又有关联,每个节点都知道下一个节点的指针,要找到下一个节点就访问那个指针就好了

        具体来讲就是把 plist 当成一个钥匙,最开始保存的是第一个节点的地址,用完第一个节点的数据之后,把第一个节点中存储的地址再给到plist,这样plist就可以开第二个节点了,以此类推···

        下面我们依照上面的图片做一个简单的节点结构

                                

        到这里,链表的地基就学会了,下面我们尝试实现一下

2. 单链表(Single linked list)的实现

        跟顺序表一样,先是把准备工作,三个文件准备好

                                

        再把链表节点写出来

                                 ​​​​​​

        在这里我要重点声明一下,接下来如果从主函数前去访问链表时用的都是plist参数,从主函数中给子函数传参也是plist,就像这样

        ​​​​​​​        ​​​​​​​                

        然后就正式进入链表实现啦,大家伙坐稳喽

2.1 链表的打印

        打印的逻辑就是先拿到第一个节点的地址 pcur,把这个地址访问到 data 打印出来,然后将pcur的内容变成下一个要打印的节点的地址,直到pcur的内容是NULL为止

        ​​​​​​​        ​​​​​​​        

2.2 链表的插入

        因为插入就一定需要申请一个新的节点,所以我们先把这个功能封装好

        向堆区申请一块空间用来存放节点,记录这个节点的地址

        当然,如果你想把newnode的类型改成 SLTNode 也可以,不过后面要用到节点地址的时候就要取地址一下,很麻烦,所以我们干脆直接返回节点的地址

2.2.1 尾插

        在链表的尾端插入一个数据。

        因为如果链表为空(没有节点)的时候要修改 plist 的内容,让它指向我们新添加的第一个节点,所以我们传参的时候要传 &plist ,因此函数参数要用二级指针来接收这个可能会被修改的plist

        如果链表不为空,就去找尾节点,把为节点的next成员内容从NULL变成我们新添加的节点地址,可以这么理解:

        这个图里有一点不恰当,就是这个 pphead 要解引用一次 (*pphead) 才能找到第一个节点的地址

        ​​​​​​​

        接下来我们运行一下看看效果

2.2.2 头插

        头插比尾插好理解一点,直接上思路图(画的太丑了QAQ)

        

        很明显,链表是否为空对于需要的操作是没有影响的,上代码:

        

        最后运行一下看结果:

        

        因为每次都是把节点插到最前面,所以反着打出来是对的

2.3 链表的删除

2.3.1 尾删

        尾删的逻辑就是找到最后一个节点 ptail 和倒数第二个节点 prev ,把倒数第二个节点的next成员置为空指针,释放掉最后一个节点。当然,如果链表为空,也就是说没有节点的话就不能执行删除操作,用assert断言报错

        ​​​​​​​        ​​​​​​​        

        上代码:

        

2.3.2 头删

        头删也是需要两个指针控制,要注意的就是要先释放掉*pphead也就是第一个节点,然后再把*pphead的内容改成第二个节点的地址,接上第二个节点

        ​​​​​​​              ​​​​​​​

        代码如下:

        ​​​​​​​        

2.4 查找

        链表的查找很简单,就是遍历链表,找到了就返回节点地址,没找到就返回空指针

        

2.5 在任意位置插入数据

2.5.1 在指定位置前插入数据

        可以用SLTFind找到要被前插的节点的地址pos,在这个节点前面插入节点,还需要直到它前面那个节点的地址prev

        ​​​​​​​        ​​​​​​​        

        在实现这个功能的时候我们要注意,当pos是头节点的情况:

        

        下面使用一下

        

2.5.2 在指定位置后插入数据

        这个比较简单,但是要注意给地址的顺序,要先把后面那个节点的地址给到新节点,再把指定位置pos节点的地址成员改成新节点的地址,否则就会导致后面那个节点地址的丢失,没办法接到新节点后面了

        还有就是我们不需要知道链表的头节点是什么了,只需要关注pos就行了

        ​​​​​​​                ​​​​​​​

        

2.6 在任意位置删除节点

2.6.1 删除pos节点

        删除pos节点要先知道它前面的那个节点prev,然后把prev跟pos后面那个节点先连起来,最后再把pos释放掉。还有要注意的一点就是当pos就是链表头节点的时候要特殊处理一下

        ​​​​​​​        ​​​​​​​          

        

2.6.2 删除pos后面的一个节点

        这个功能也是只需要关注pos后面的内容就行,所以只需要传pos一个参数。还要注意一点就是pos不能是链表中的最后一个节点,否则它后面没有节点了还删什么

        ​​​​​​​        

        ​​​​​​​        

2.7 链表的销毁

        两个变量,pcur记录当前要准备销毁的节点地址,next记录下一个节点地址,防止销毁上一个节点之后找不到下一个节点了。然后两个变量一直循环向后扫描销毁,直到pcur指向NULL

        ​​​​​​​                ​​​​​​​

                        

3. 链表的分类

        链表按带头或不带头,单向或双向,循环或不循环,排列组合有8种

        我们刚刚学的单链表全称就是:不带头单向不循环链表

        带头不带头是说链表有没有一个不存储有效数据的节点,放在第一个存放有效数据节点之前

        ​​​​​​​        ​​​​​​​        

        单向双向是说链表能通过后一项找到前一项就是双向的,如果只能根据前一项找到后一项链表就是单项的。或者说双向链表的节点中的两个存放地址的成员中,一个存下一个节点的地址,一个存上一个节点的地址。

        ​​​​​​​        ​​​​​​​        

        循环不循环是说最后一个节点指向第一个节点就是循环链表,要是最后一个节点指向NULL就是不循环链表

                        

        虽然链表的种类很多,但是常用的只有两种:

                1.单链表(不带头单向不循环链表)

        单链表结构简单,一般不会单独用来存贮数据,它一般作为其他数据结构的子结构出现

                2.双向链表(带头双向循环链表)

        双向链表结构最复杂,一般用来单独存储数据。它虽然复杂,但是之后实现它的实际就会发现它有很多优势,致使实现它反而变得简单了,后面会有实现它的章节的。

4. 本节代码

        SList.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

//链表是由节点构成的
typedef int SLTDataType;

typedef struct SListNode 
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;


//链表的打印
void SLTPrint(SLTNode* phead); 

//链表的插入
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);

//链表的头删和尾删
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);


//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x);


//在任意位置插入数据
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

//在任意位置删除节点
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos后面的一个节点
void SLTEraseAfter(SLTNode* pos);

//销毁链表
void SLTDestory(SLTNode** pphead);

        SList.c

#include"SList.h"

//链表的打印	
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}


//申请一个新节点
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}


//链表的插入
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//申请一个新节点
	SLTNode* newnode = SLTBuyNode(x);

	//链表为空,新节点作为头
	if (*pphead == NULL)
	{
		*pphead = newnode;
		return;
	}

	//链表不为空,找尾节点
	SLTNode* ptail = *pphead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	//找到尾节点了
	ptail->next = newnode;
}

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//申请一个新节点
	SLTNode* newnode = SLTBuyNode(x);

	//链表为不为空,操作都一样
	//斩断第一个连接,再把新节点接进去
	newnode->next = *pphead;
	*pphead = newnode;
}




//链表的头删和尾删
//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	//链表不能为空
	assert(*pphead);

	//链表不为空
	//链表只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//链表有多个节点
	SLTNode* ptail = *pphead;
	SLTNode* prev = NULL;
	//找到尾节点
	while (ptail->next)
	{
		prev = ptail;
		ptail = ptail->next;
	}
	prev->next = NULL;
	free(ptail);
	ptail = NULL;
	
}

//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	//链表不能为空
	assert(*pphead);

	//让第二个节点变成新的头节点
	//释放旧的头节点
	SLTNode* sec = (*pphead)->next;
	free(*pphead);
	*pphead = sec;
}



//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//遍历链表
	SLTNode* pcur = *pphead;

	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}

	return NULL;
}




//在任意位置插入数据
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	//这里断言*pphead不能为空
	//因为指定节点的地址pos不能为空,所以这个链表也不能为空
	assert(*pphead);

	//当pos是头节点,执行头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
		return;
	}

	//当pos不是头节点
	//申请一个新节点
	SLTNode* newnode = SLTBuyNode(x);

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

//在指定位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);

	//申请一个新节点
	SLTNode* newnode = SLTBuyNode(x);

	newnode->next = pos->next;
	pos->next = newnode;
}




//在任意位置删除节点
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);

	//如果pos指向头节点,执行头删
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
		return;
	}

	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

//删除pos后面的一个节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	//pos->next不能为空
	//就是说pos不能是最后一个节点
	assert(pos->next);

	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}



//销毁链表
void SLTDestory(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);

	SLTNode* pcur = *pphead;
	SLTNode* next = NULL;
	while (pcur)
	{
		next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

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

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

相关文章

【C++】介绍STL中list容器的常用接口

目录 一、STL中的list简介 二、构造函数 2.1 默认构造函数 2.2 填充构造&#xff08;用n个相同的值构造&#xff09; 2.3 迭代器构造 2.4 拷贝构造和赋值运算符重载 三、迭代器 3.1 正向迭代器 3.2 反向迭代器 四、容量相关 4.1 获取list中有效数据的个数 4.2 判…

【Unity学习笔记】第十一 · 动画基础(Animation、状态机、root motion、bake into pose、blendTree、大量案例)

转载引用请注明出处&#xff1a;&#x1f517;https://blog.csdn.net/weixin_44013533/article/details/132081959 作者&#xff1a;CSDN|Ringleader| 如果本文对你有帮助&#xff0c;不妨点赞收藏关注一下&#xff0c;你的鼓励是我前进最大的动力&#xff01;ヾ(≧▽≦*)o 主…

【Python编程工具】【ssh连接Docker容器】如何使用Docker容器里的python环境,如何调试在容器中的代码

文章目录 方案一览Gateway软件介绍启动容器配置apt源在容器中安装SSH服务器配置SSH服务器生成SSH密钥启动SSH服务为root创建密码连接到容器使用Gateway 方案一览 本篇博客将介绍如何在Docker容器中打开SSH连接服务&#xff0c;以及如何使用JetBrains Gateway软件进行代码调试。…

数据结构与算法-二叉树-路径总和 II

路径总和 II 给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root [5,4,8,11,null,13,4,7,2,null,null,5,1], target…

wpf控件Expander集合下的像素滚动

项目场景&#xff1a;Expander集合滚动 如下图&#xff0c;有一个Expander集合&#xff0c;且设置 ScrollViewer.VerticalScrollBarVisibility "Auto" 每个Expaner下包含有若干元素&#xff0c;当打开Expader(即IsExpanded "true"&#xff09;时&#…

企业邮箱遭入侵!印度制药巨头损失超4500万元

近日&#xff0c;印度制药巨头阿尔肯实验室子公司部分员工的企业邮箱遭入侵&#xff0c;导致其子公司被欺诈5.2亿卢比&#xff08;约合人民币4500万元&#xff09;。而根据截至2023年9月的季度财务报告数据&#xff0c;该公司营业收入为263.46亿卢比&#xff0c;净利润为64.65亿…

电脑城衰退的原因是什么?

​电脑城衰退的原因分析 随着科技的飞速发展&#xff0c;电脑城曾经是电子产品交易的热门场所。然而&#xff0c;近年来&#xff0c;电脑城的发展状况不容乐观&#xff0c;正面临着巨大的挑战。究竟是什么原因导致了电脑城的衰退&#xff1f;本文将深入探讨这一问题。 电子商…

05-Seata下SQL使用限制

不支持 SQL 嵌套不支持多表复杂 SQL(自1.6.0版本&#xff0c;MySQL支持UPDATE JOIN语句&#xff0c;详情请看不支持存储过程、触发器部分数据库不支持批量更新&#xff0c;在使用 MySQL、Mariadb、PostgreSQL9.6作为数据库时支持批量&#xff0c;批量更新方式如下以 Java 为例 …

编译安装Nginx和使用五种算法实现Nginx反向代理负载均衡

目录 Ubuntu中安装Nginx 概念介绍 负载均衡 几种负载均衡算法 反向代理 环境规划 配置反向代理 加权负载均衡&#xff08;Weighted Load Balancing&#xff09; 轮询&#xff08;Round Robin&#xff09; IP 哈希&#xff08;IP Hash&#xff09; 最少连接&#xff…

服务器和云桥通SDWAN组网的区别

一、服务器的概念 服务器是一种计算设备&#xff0c;用于存储、处理和提供数据和应用服务。通常&#xff0c;服务器配备高性能处理器、大容量存储器和网络接口&#xff0c;其主要目的是提供计算资源、存储资源以及应用程序的托管。这种设备可以用于托管网站、应用程序、数据库和…

1331:【例1-2】后缀表达式的值

【题目描述】 从键盘读入一个后缀表达式&#xff08;字符串&#xff09;&#xff0c;只含有0-9组成的运算数及加&#xff08;&#xff09;、减&#xff08;—&#xff09;、乘&#xff08;*&#xff09;、除&#xff08;/&#xff09;四种运算符。每个运算数之间用一个空格隔开…

8.8加油站(LC134-M)

算法&#xff1a; 首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈&#xff0c; 每个加油站的剩余量rest[i]为gas[i] - cost[i]。 说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。 i从0开始累加rest[i]&#xff0c;和记为curSum&#xff0c;一旦curS…

合合信息启信数据发布园区金融解决方案,助力银行精准服务“十四五”特色产业

今年冬季寒潮频现&#xff0c;“尔滨”等冰雪之城却凭借着出色的文旅服务&#xff0c;接连火爆“出圈”。现阶段&#xff0c;作为传统工业基地的哈尔滨正积极向第三产业转型。文旅园区具备产业、技术、知识、劳动力密集属性和特定产业集群规模效应&#xff0c;是推动文化与创意…

腾讯云安装Java11(jdk11.0.21)

腾讯云安装Java11(jdk11.0.21) 下载Java11 下载Linux的jdk包Java11下载路径 https://www.oracle.com/java/technologies/javase/jdk11-archive-downloads.html 解压jdk 下载完成后&#xff0c;进入自己想要放到的目录下面&#xff0c;输入tar -zxvf jdk-11.0.21_linux-x64_b…

考研C语言刷题基础篇之数组(一)

目录 第一题&#xff1a;用数组作为参数实现冒泡排序 不用函数的冒泡排序 冒泡排序原理&#xff1a; 错误的数值传参冒泡排序 错误的原因 就是什么是数组名 正确的数组传参的冒泡排序 数组的地址和数组首元素的地址的区别 第一题&#xff1a;用数组作为参数实现冒泡排…

oracle19.22的patch已发布

2024年01月16日,oracle发布了19.22的patch 具体patch如下 Reserved for Database - Do not edit or delete (Doc ID 19202401.9) 文档ID规则如下 19(版本)+年份(202x)+(季度首月01,04,07,10).9 往期patch no信息和下载参考文档 oracle 19C Release Update patch num…

喝葡萄酒要懂得选对杯

喝葡萄酒要懂得选对杯 一、什么是葡萄酒杯&#xff1f; 葡萄酒杯&#xff0c;因其有一个细长的底座而被大众形象的称为高脚杯&#xff0c;但在事实上&#xff0c;高脚杯只是葡萄酒杯中的一种。在葡萄酒文化中&#xff0c;酒杯是其不可缺失的一个重要环节&#xff0c;在西方传统…

《动手学深度学习(PyTorch版)》笔记3.1

Chapter3 Linear Neural Networks 3.1 Linear Regression 3.1.1 Basic Concepts 我们通常使用 n n n来表示数据集中的样本数。对索引为 i i i的样本&#xff0c;其输入表示为 x ( i ) [ x 1 ( i ) , x 2 ( i ) , . . . , x n ( i ) ] ⊤ \mathbf{x}^{(i)} [x_1^{(i)}, x_2…

composer安装hyperf后,nginx配置hyperf

背景 引入hyperf项目用作微服务&#xff0c;使用composer 安装hyperf后&#xff0c;对hyperf进行nginx配置。 配置步骤 因为hyperf监听的是端口&#xff0c;不像其他laravel、lumen直接指向文件即可。所有要监听端口号。 1 配置nginx server {listen 80;//http&#xff1a…

逻辑回归中的损失函数梯度下降

一、引言 逻辑回归中的损失函数通常采用的是交叉熵损失函数&#xff08;cross-entropy loss function&#xff09;。在逻辑回归中&#xff0c;我们通常使用sigmoid函数将线性模型的输出转换为概率值&#xff0c;然后将这些概率值与实际标签进行比较&#xff0c;从而计算损失。 …