线性表之单链表

news2024/9/26 21:55:18

在上一节我们学习了线性表中的顺序表,今天我们来学习一下线性表中的另一种结构——单链表


前言

我们在之前已经初步了解了数据结构中的两种逻辑结构,但线性结构中并非只有顺序表一种,它还有不少兄弟姐妹,今天我们再来学习一下单链表。

一、单链表是什么?

单链表是一种基础的数据结构,用于存储一系列元素。它由一系列节点(node)组成,每个节点包含两个部分:一个数据部分和一个指向下一个节点的指针部分。它与我们上一节学习的顺序表有所不同,仅能根据数组的下标来确定位置,单链表它能够根据它结点中的指针来找寻下一个结点,这样无形之中就能提高存取数据的灵活性了。

与顺序表不同的是,链表中的元素的存储空间都是独立申请下来的,我们称之为“结点”,结点一般都是从堆上申请下来的(一般通过malloc,realloc等动态内存分配函数来进行申请空间的),这些从堆上申请的空间,是按照一定的策略分配出来的,每次申请的空间可能连续,也可能不连续。这就是它在物理结构上不连续的一个原因了。

结点中包含两个域:其中存储元素信息的域称为数据域;存储直接后继存储位置的域称为指针域

对于这个结点,还有不少学问呢!接下来我们来对首元结点,头结点,头指针3个容易混淆的概念进行说明。

(1)首元结点是用来存储链表中第一个数据元素的结点。

(2)头结点是在首元结点之前的一个结点,它的指针域指向首元结点的位置,头结点的数据域中可以不存放任何信息,也可以存储与数据元素类型相同的其他附加信息。例如当数据元素为整型时,头结点的数据域可以存放该链表的长度(因为长度一般是整型类型)。

(3)头指针是指向链表中第一个结点的指针,若链表中设有头结点,那么头指针就指向头结点,若没有设头结点,头指针就直接指向首元结点。

这时候,就会有人要问了:既然已经有了首元结点了,为啥还要设置一个头结点呢?这不是画蛇添足嘛。现在,我来给你们介绍一下有头结点的好处:

1)便于对首元结点的处理:有了头结点之后,我们能够更好地处理有关首元结点的操作,比如插入删除等操作,我们也不必为它特意写个函数来实现这些功能了,我们使用正常的插入删除操作就能够实现它了。

2)便于对空表与非空表的统一处理:当链表不设置头结点时,假设L为该链表的头指针,它应该指向首元结点,当单链表的长度为0时的空表时,L指针为空(判定链表为空表的条件就是:L==NULL)我们要知道,我们一般都是将结点的指针域指向NULL的,现在咱们将头指针指向NULL,那么我们就会造成一个误解:这个头指针是个NULL。当我们增加头结点之后,无论链表是否为空,头指针都是指向首元结点的非空指针。(判定链表为空表的条件就是:L->next==NULL)下图是设有头结点的单链表

二、单链表与顺序表的比较

首先我们先对它们的概念进行一下对比:

单链表:单链表是逻辑结构上连续,物理结构上不连续的数据结构,数据元素中的逻辑顺序是通过链表中的指针链接次序实现的。

顺序表:顺序表是逻辑结构上连续,物理结构上也连续的数据结构,数据元素的逻辑顺序是通过数组下标进行实现的。

空间性能的比较

(1)存储空间的分配:顺序表的存储空间是必须预先分配的,元素个数具有一定限制,容易造成空间浪费或者空间溢出的情况;而链表可以根据数据元素来进行分配空间,只要内存空间允许,链表中的元素个数就没有限制,可以说有几个元素给几个结点空间。

(2)存储密度的大小:由于链表中除了设置了数据域还设置了指针域,用来存储元素之间逻辑关系的指针。从存储密度上来说,这是不经济的。所谓存储密度就是存放数据的空间占据结点空间的比例

当存储密度越高,那么存储空间的利用率就越高。由此我们可以知道,顺序表的存储利用率是100%,因为它整个结点都存放着数据,而链表中由于存放了指针域,那么它的存储利用率就小于100%。

由上面的两个比较,我们可以得出一个结论:当线性表的长度较大且难以估测存储规模时,宜采用链表作为存储结构;当线性表的长度不大且我们事先已经知道其具体大小时,为了节约存储空间,宜采用顺序表来作为存储结构。

时间性能的比较

(1)存取元素的效率:由于上面两种的物理结构有所不同,它们的存取方式也有所不同。其中,顺序表是随机存取(因为它的底层基础是数组,数组具有下标,当我们想要查找某个元素时,可以直接根据数组下标来查找相应的元素),链表是顺序存取(因为链表是由一个个结点通过结点中的指针域链接而成的,因此我们每次在查找某个元素时,只能够通过指针从首元结点开始逐个遍历来找到相应的元素)这里它们两个的时间复杂度也不同,前者是O(1),后者是O(N)。

(2)插入与删除操作的效率:对于链表已经确定的元素插入删除的位置后,插入删除操作无须移动数据,只要修改指针即可,时间复杂度为O(1)。而对于顺序表,即使已经知道要插入删除的位置之后,我们在进行插入删除操作时,仍要进行大量元素的移动来实现,时间复杂度为O(N)。而且当每个结点的信息量较大时,移动结点的时间开销就很多了。因此对于频繁进行插入删除操作的线性表,宜采用链表作为存储结构。

三、单链表的实现

接下来我们来介绍一下如何来实现一个单链表,接下来我将我写的源代码与一些注释附上。与顺序表一样,我们也将单链表分为三个文件:SList.h    ,SList.c,    test.c。

SList.h

#pragma once
typedef int SLTDatatype; //定义一个数据变量,方便后面一键替换数据类型

//定义一个单链表结点
typedef struct SListNode
{
	SLTDatatype data;//存放数据
	struct SListNode* next;//指向下一个结点的地址
}SLTNode;

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


//插入
//插入新结点(每次插入前要申请一个新的结点)
//由于头插,尾插都有可能涉及到头指针,因此形参我们要使用二级指针,实参要传递的是一级指针的地址
void SLTPushBack(SLTDatatype**pphead,SLTDatatype x);
void SLTPushFront(SLTDatatype**pphead, SLTDatatype x);

//删除
void SLTPopBack(SLTNode**pphead);
void SLTPopFront(SLTNode**pphead);

//查找
SLTNode* SLFind(SLTNode* phead,SLTDatatype x);

//在指定位置之前插入数据
void SLTInsert(SLTDatatype** pphead, SLTNode* pos, SLTDatatype x);

//在指定位置之后插入数据
void SLTInsertAfter(SLTDatatype** pphead, SLTNode* pos, SLTDatatype x);

//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);

//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);

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

由上面的代码,我们可以看出,我们在定义一个结点的时候,在结构体中只有两个成员元素:数据域,指针域。另外,这里函数里面传递的参数我们也要注意一下:与之前的顺序表不同,之前的顺序表我们是由数组来实现的,因此我们传递参数时,直接就传递了链表的指针变量即可,但是现在我们在链表中,我们本身就是通过指针来找寻下一个结点的位置,因此我们在传递参数的时候我们要传递的是链表指针的地址,我们在之前学过:存放一级指针的地址的指针叫做二级指针。于是我们的参数传递的就是一个二级指针。(注意:我们传递二级指针作为参数的一定是在那个函数中,我们需要对那个头指针进行相关解引用操作)

SList.c

#define  _CRT_SECURE_NO_WARNINGS
#include"SList.h"
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

//申请一个新结点
SLTNode* SLTBuyNode(SLTDatatype x)
{
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTDatatype));
	if (node  == NULL)
	{
		perror("malloc");
		exit(1);
	}
	//将结点中的数据内容初始化
	node->data = x;
	node->next = NULL;

	return node;
}


//打印链表
void SLTPrint(SLTNode* phead)
{
	//定义一个指针,后面来遍历链表。初始位置指向phead头指针
	SLTNode* pcur = phead;
	while (pcur)    //pcur!=NULL
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;   //使指针pcur不断向后移动
	}
	printf("NULL\n");//链表的最后一个结点的指针域指向NULL的
}

//尾插
void SLTPushBack(SLTDatatype** pphead, SLTDatatype x)
{
	assert(pphead);//防止头指针的地址找不到

	//pphead这是二级指针,即pphead==&phead
	//*pphead==phead(这就是头指针的指针,存放着头指针的地址),**pphead==*phead即头指针地址指向的那个结点

	SLTNode* newnode = SLTBuyNode(x);//定义一个新结点,等会进行插

	if (*pphead == NULL)
	{
		//这种情况是空链表,因此头指针指向NULL,然后直接加入新节点
		*pphead = newnode;
	}
	else
	{
		//这种情况是不是空链表,因此我们要找插入的位置
		//首先找一个尾结点,然后在尾结点后面进行插入新结点,即尾插
		SLTNode* pcur = *pphead;  //这里定义一个新的指针是为了后面方便找尾结点的,必须将其初始化为头指针,否则它不是从头指针开始遍历查找
		while (pcur->next ) //这里的条件是为了找尾结点,只要这个结点的下一个结点是一个NULL。那么就可以确认了这是一个尾结点
		{
			pcur = pcur->next;
		}//pcur nownode
		pcur->next = newnode;//将新节点的地址传递给尾结点的指针域,那么newnoda就变成尾结点了
	}
}

//头插
void SLTPushFront(SLTDatatype** pphead, SLTDatatype x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);

	newnode->next = *pphead;// 我们将新插入的新节点的指针域指向头指针,注意:我们要赋值的是头指针(已经是一个地址了,如果我们直接写pphead,这是头指针的地址)
	*pphead = newnode;      //然后将新节点的地址赋给头指针,即新节点作为头指针

}

//头删
void SLTPopFront(SLTNode**pphead)
{
	assert(pphead && *pphead);//判断不是一个空链表且头指针的地址要存在

	SLTNode* next = (*pphead)->next;  //在删除之前,咱们可以先用一个next结点将头指针下一个结点的地址保存下来
	free(*pphead);       //将头指针删除
	*pphead = next;      //再将下一个结点作为头指针

}


//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	//尾删分两种情况:一种:只有一个结点,直接删除释放;一种:有好几个结点,我们要先找到最后一个结点,然后再进行删除
	if ((*pphead)->next == NULL)//如果下一个结点是一个NULL,那么这只有一个结点
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		//这种情况我们要找到最后一个结点并且将它删掉,因此我们还要找到最后一个结点的前一个结点
		SLTNode* ptail = *pphead;  //最后一个结点
		SLTNode* prev = NULL;      //最后一个结点的前一个结点
		while (ptail->next )
		{
			prev = ptail;          //最后一个结点的位置给它上一个结点
			ptail = ptail->next;   //结点不断向后移动

		}
		prev->next = NULL;   //将上一个结点的指向的内容设为NULL,因为此时已经是尾结点了
		free(ptail);
		ptail = NULL;
	}
}

//查找
SLTNode* SLFind(SLTNode* phead,SLTDatatype x)
{
	assert(phead);   //判断链表要不为空
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}


//在指定位置之前插入数据
void SLTInsert(SLTDatatype** pphead, SLTNode* pos, SLTDatatype x)
{
	assert(pphead && pos);
	if (pos == *pphead)
	{
		SLTPopFront(**pphead);
	}
	else
	{
		//先创建一个新节点
		SLTNode* newnode = SLTBuyNode(x);
		SLTNode* prev = *pphead;
		while (prev->next=pos )
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
}

//在指定位置之后插入数据
void SLTInsertAfter(SLTDatatype** pphead, SLTNode* pos, SLTDatatype x)
{
	assert(pphead && pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

//删除pos结点
void SLTErase(SLTNode**pphead, SLTNode* pos)
{
	assert(pphead && pos &&*pphead );
	if (pos==*pphead)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//prev pos pos->next
		prev->next  = pos->next;
		free(pos);
		pos = NULL;
	}
}

//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next );
	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

//销毁链表
void SLDestroy(SLTNode**pphead)
{
	assert(*pphead && pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

在这个文件中,我们所要实现的就是单链表。在这里面有些我重点拿出来讲讲(其实在上面的源代码中的注释已经很详细了)

(1)我们要有一个创建一个新结点的操作,因为在后续操作中,插入首先都是创建一个新结点,然后再进行插入;

(2)我们如果想要查找某个元素或者在某个位置插入删除,在此之前,我们要新定义一个新的指针来遍历链表,找到自己想要的位置,我们在定义的时候一般都是将该指针指向头指针的位置;

(3)我们在进行删除操作的时候,有时候我们如果想要释放某个结点空间的时候,我们在移动链表之前,一定要定义一个新的指针来存放那个将要删除的结点地址,因为我们要知道一旦我们移动链表,如果将那个结点覆盖掉了就没了,我们因此就无法释放那个空间了;

test.c

#define  _CRT_SECURE_NO_WARNINGS
#include"SList.h"
#include<stdio.h>
#include<stdlib.h>


void SLTtest()
{
	SLTNode* plist = NULL;//定义初始化一个头指针

	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTNode*find= SLFind(plist, 33);
	if (find == NULL)
	{
		printf("没找到\n");
	}
	else 
	{
		printf("找到了\n");
	}
	SLDestroy(&plist);
	SLTPrint(plist);

	//头插
	SLTPushFront(&plist, 9);
	SLTPushFront(&plist, 8);
	SLTPushFront(&plist, 7);
	SLTPushFront(&plist, 6);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);

}

int main()
{
	SLTtest();
	return 0;
}

上面这个文件,我们是用来进行测试,我们在写好某个功能之后,可以到这个test.c进行测试一下,咱们一部分一部分地测试,最后咱们就能写好一个单链表了。

总结

我们这节学习的单链表与上一节学习的顺序表有着不少相似之处,但是二者的区别也是很大的,希望大家能够熟练掌握这两种数据结构的实现。最后告诉大家:孰能生巧!

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

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

相关文章

RealityCapture全面讲解:摄影测量软件的新纪元

随着数字化技术的迅猛发展&#xff0c;摄影测量软件在各行各业中的应用日益广泛。其中&#xff0c;RealityCapture作为一款领先的摄影测量解决方案&#xff0c;以其卓越的速度、精度和易用性&#xff0c;赢得了全球众多专业人士的青睐。本文将全面讲解RealityCapture的功能特点…

演示:基于WPF自绘的中国省份、城市、区县矢量地图

一、目的&#xff1a;演示一个基于WPF自绘的中国省份、城市、区县矢量地图 二、效果 国 省 市 三、功能 支持实际经纬度显示 支持平移&#xff0c;缩放等功能 显示中国地图 显示各个省份地图 显示各个省份地图&#xff08;包含在表格中&#xff0c;包含缩率图&#xff09; 显…

UE4_后期处理五—饱和度调整、隔离、扭曲、重影

一、色彩饱和度调整&#xff1a; 原图 后期处理材质节点&#xff1a; 效果图&#xff1a; 可以根据参数saturation调整饱和还是去饱和。 当saturation为1时&#xff1a;去饱和度&#xff0c;如下图&#xff1a; 当saturation为0时&#xff1a;原始的一个状态&#xff0c;如下…

JS import export export default ES6 modules 玩的明白吗

export (ES6) 导出 一个文件可以有多个&#xff0c;不可重名 命名导出&#xff1a; 使用export关键字导出变量、函数、类或值时&#xff0c;需要为它们指定名称。这些名称将在其他模块中用于导入。 export default 单一导出&#xff1a; export default 只能用于导出一个模块、…

python 读取excel

一、安装依赖&#xff1a; pandas 二、新建excel 示例数据&#xff1a;students.xlsx 三、定义类&#xff1a;student.py Student class Student:def __init__(self, name, sex):self.name nameself.sex sexdef show(self):print(f姓名&#xff1a;{self.name} 性别&#…

全面理解tensor编程中矩阵的行和列

经常会在编程中遇到理解矩阵行和列的事情。 1、要明确无论这个张量有多少维度&#xff0c;它的矩阵乘法都只能作用于最后两个维度。 例如&#xff1a; import torcha torch.rand([64, 32, 3, 4]) b torch.rand([64, 32, 3, 4])c torch.matmul(a, b.transpose(2, 3)) # 交…

3.接口测试的基础/接口关联(Jmeter工具/场景一:我一个人负责所有的接口,项目规模不大)

一、Jmeter接口测试实战 1.场景一&#xff1a;我一个人负责所有的接口&#xff1a;项目规模不大 http:80 https:443 接口文档一般是开发给的&#xff0c;如果没有那就需要抓包。 请求默认值&#xff1a; 2.请求&#xff1a; 请求方式:get,post 请求路径 请求参数 查询字符串参数…

sh文件执行提示语法错误: 未预期的文件结尾

在执行sh文件时总是提示&#xff1a;语法错误: 未预期的文件结尾&#xff0c;尝试删除最后的空格也不对 最后发现在notepad中转换的问题 需要把windows换成unix就行了

时间序列中的多尺度问题-近期值得关注的8篇多尺度建模工作

时间序列的多尺度建模 多尺度是时序研究必须要考虑的问题。一方面&#xff0c;不同特征的周期模式有长有短&#xff0c;需要用不同尺度进行刻画。另一方面&#xff0c;尺度越小越精细&#xff0c;计算越复杂&#xff1b;尺度越大越粗糙&#xff0c;相应计算量减少&#xff0c;…

容器化安装jenkins稳定版长期维护版本LTS

前提已有 docker-compose和docker-ce环境&#xff0c;这里安装稳定的Lts版本即可。 选择稳定版本 这里选择LTS 稳定长期维护的版本 在docker镜像找到LTS稳定版本 部署jenkins服务 创建持久化数据目录 jenkinsdata]# pwd /data/jenkinsdata编写docker-compose文件 jenkins_…

DAY 13 : 排序

定义 稳定排序和非稳定排序 设文件f&#xff08;R1……Ri……Rj……Rn&#xff09;中记录Ri、Rj&#xff08;i≠j&#xff0c;i、j1……n&#xff09;的key相等&#xff0c;即KiKj。 若在排序前Ri领先于Rj&#xff0c;排序后Ri仍领先于Rj&#xff0c;则称这种排序是稳定的&…

Linux操作系统入门(三)

_______________________________________________ 一.Linux操作系统的文件结构 相比于Windows操作系统的C,D,E等盘符&#xff0c;Linux操作系统仅有一个"/"符号的根目录. 这其中存在一个显著的不同&#xff0c;Linux操作系统使用的是斜杠"/",而Windows…

【LLM多模态】文生视频评测基准VBench

note VBench的16个维度自动化评估指标代码实践&#xff08;待完成&#xff09;16个维度的prompt举例人类偏好标注&#xff1a;计算VBench评估结果与人类偏好之间的相关性、用于DPO微调 文章目录 note一、相关背景二、VBench评测基准概述&#xff1a;论文如何解决这个问题&…

AJAX 入门 day1

目录 1.AJAX 概念和 axios 使用 2.认识 URL 3.URL 查询参数 4.常用请求方法和数据提交 5.HTTP协议-报文 5.1 HTTP 协议&#xff0d;请求报文 5.2 HTTP 协议&#xff0d;响应报文 6.接口文档 7.案例 - 用户登录 8.form-serialize 插件 1.AJAX 概念和 axios 使用 “Aj…

华为OD机试 - 找出作弊的人(Java 2024 E卷 100分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;E卷D卷A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加…

JNI 详细介绍

一 介绍 java调⽤c&#xff0c;c代码可以通过JNIEnv执行java代码。 安卓NDK 已经对JNI环境进行了集成&#xff0c;我们可以通过android studio来快速搭建一个项目。 二 项目搭建 打开android studio 创建工程&#xff0c;创建工程选择模板Native C 三 模板格式介绍 生成的…

char 的整数存储解释

目录 1.原因讲解 1.有符号类型的char 2.无符号类型的char(0-255) 练习题1. 练习题2. 练习题3. 小心&#xff01;VS2022不可直接接触&#xff0c;否则&#xff01;没这个必要&#xff0c;方源面色淡然一把抓住&#xff01;顷刻炼化&#xff01; 1.原因讲解 1.有符号类型的…

AUTOSAR_EXP_ARAComAPI的5章笔记(6)

返回目录 5.3.5.5 Event-Driven vs Polling-Based access ara::com实现完全支持事件驱动和轮询的方式来访问新数据。 对于轮询方式&#xff0c;典型的用例是&#xff0c;一个应用程序被周期性地触发并在特定的截止时间前进行一些处理。这是调节器/控制算法的典型模式 —— 循…

如何清除KylinOS桌面操作系统桌面密钥环?

如何清除KylinOS桌面操作系统桌面密钥环&#xff1f; 1、问题现象2、解决 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 1、问题现象 打开桌面应用会出现【解锁秘钥环提示】&#xff1a; 2、解决 1、安装seahorse sudo apt-get insta…

【JavaEE】IP协议 应用层协议

&#x1f525;个人主页&#xff1a; 中草药 &#x1f525;专栏&#xff1a;【Java】登神长阶 史诗般的Java成神之路 &#x1f576;️一.IP地址 IP协议&#xff08;Internet Protocol&#xff09;是TCP/IP协议族中最核心的协议之一&#xff0c;它定义了数据包在网络中传输的标准…