线性表之单链表

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


前言

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

一、单链表是什么?

单链表是一种基础的数据结构,用于存储一系列元素。它由一系列节点(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 nownodepcur->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->nextprev->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.mzph.cn/pingmian/53936.shtml

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

相关文章

Python基础语法(3)下

列表和元组 列表是什么&#xff0c;元组是什么 编程中&#xff0c;经常需要使用变量&#xff0c;来保存/表示数据。变量就是内存空间&#xff0c;用来表示或者存储数据。 如果代码中需要表示的数据个数比较少&#xff0c;我们直接创建多个变量即可。 num1 10 num2 20 num3…

[数据集][目标检测]葡萄成熟度检测数据集VOC+YOLO格式1123张3类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1123 标注数量(xml文件个数)&#xff1a;1123 标注数量(txt文件个数)&#xff1a;1123 标注…

【重学 MySQL】二十九、函数的理解

【重学 MySQL】二十九、函数的理解 什么是函数不同 DBMS 函数的差异函数名称和参数功能实现数据类型支持性能和优化兼容性和可移植性 MySQL 的内置函数及分类单行函数多行函数&#xff08;聚合函数&#xff09;使用注意事项 什么是函数 函数&#xff08;Function&#xff09;在…

秋韵虫趣.

文章目录 虫鸣概览虫坛文化蟀种纷呈中华蟋蟀宁阳蟋蟀刻点铁蟋长颚斗蟋 油葫芦棺头蟋中华灶蟋小素蟋树皮蟋蟀 花生大蟋斑腿针蟋其他鸣虫树蟋&#xff0c;又名竹蛉、邯郸梨片蟋&#xff0c;又名金钟、天蛉、绿蛣蛉、银琵琶凯纳奥蟋&#xff0c;又名石蛉&#xff0c;鳞蟋黄蛉蟋&am…

Python | Leetcode Python题解之第409题最长回文串

题目&#xff1a; 题解&#xff1a; class Solution:def longestPalindrome(self, s: str) -> int:ans 0count collections.Counter(s)for v in count.values():ans v // 2 * 2if ans % 2 0 and v % 2 1:ans 1return ans

本地部署大模型并使用知识库Windows下Ollama+Docker+MaxKB安装的记录

概要 本文介绍本地部署大模型和知识库的小白方法&#xff0c;可以运行较多种类的大模型&#xff0c;使用的软件为docker和ollama以及MaxKb作为知识库前端。 下载 各安装包可以百度去官网或者github下载或使用&#xff0c;也可以点击下面的的链接和我下载相同的版本。 ollama…

Lnux-gcc/g++使用

目录 1.gcc/g介绍 1.什么是 gcc / g 2.gcc/g指令格式 2. gcc / g 实现程序翻译的过程 1.预处理(进行宏替换) 2.编译(生成汇编&#xff09; 3.汇编&#xff08;生成机器可识别代码&#xff09; 4.连接&#xff08;生成可执行文件或库文件&#xff09; 1.gcc/g介绍 1.什么…

小明震惊OpenAI 的新模型 01

在硅谷的中心&#xff0c;繁忙的咖啡馆和创业中心周围&#xff0c;年轻的软件工程师小明坐在他的办公桌前&#xff0c;面露困惑。科技界一直在盛传一项新的AI突破&#xff0c;但他持怀疑态度&#xff0c;不敢抱太大希望。他认为AI泡沫即将破灭&#xff0c;炒作列车即将出轨&…

【计算机网络】网络通信中的端口号

文章目录 一、引入端口号二、端口号的作用三、端口号的确定 在TCP/IP协议中&#xff0c;传输层有两个重要的协议&#xff1a;TCP&#xff08;传输控制协议&#xff09;和UDP&#xff08;用户数据报协议&#xff09;。TCP用于提供可靠的数据传输&#xff0c;而UDP则适合用于广播…

电子电气架构 --- 基于ISO 26262的车载电子软件开发流程

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 屏蔽力是信息过载时代一个人的特殊竞争力&#xff0c;任何消耗你的人和事&#xff0c;多看一眼都是你的不…

《ImageNet Classification with Deep Convolutional Neural Networks》论文导读

版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl《ImageNet Classification with Deep Convolutional Neural Networks》是一篇在深度学习领域具有重要影响力的论文,由Alex Krizhevsky、Ilya Sutskever和Geoffrey E. Hinton等人撰写。该论文主要…

golang实现正向代理http_proxy和https_proxy

package mainimport ("bytes""fmt""io""log""net""net/url""strings" )func main() {// tcp 连接&#xff0c;监听 8080 端口l, err : net.Listen("tcp", ":8080")if err ! nil {l…

FTP、SFTP安装,整合Springboot教程

文章目录 前言一、FTP、SFTP是什么&#xff1f;1.FTP2.SFTP 二、安装FTP1.安装vsftp服务2.启动服务并设置开机自启动3.开放防火墙和SELinux4.创建用户和FTP目录4.修改vsftpd.conf文件5.启动FTP服务6.问题 二、安装SFTP1、 创建用户2、配置ssh和权限3、建立目录并赋予权限4、启动…

Wophp靶场寻找漏洞练习

1.命令执行漏洞 打开网站划到最下&#xff0c;此处的输入框存在任意命令执行漏洞 输入命令whoami 2.SQL注入 搜索框存在SQL注入&#xff0c;类型为整数型 最终结果可以找到管理员账户和密码 3.任意文件上传漏洞 在进入管理员后台后&#xff0c;上传木马文件 访问该文件&…

博客系统测试报告

当我们至少完成了一次项目的功能测试后&#xff0c;我们可以写一篇测试报告出来。在这里&#xff0c;我先完成了功能测试&#xff0c;自动化测试&#xff0c;又进行了弱网测试&#xff0c;我们把它们都编入测试报告&#xff0c;来写出一篇简单的博客系统测试报告 Gitee源码&am…

树莓派5上手

1 安装系统 Raspberry Pi OS 是基于 Debian 的免费操作系统&#xff0c;针对 Raspberry Pi 硬件进行了优化。Raspberry Pi OS 支持超过 35,000 个 Debian 软件包。树莓派 5 可以安装各种系统&#xff0c;但是如果对于系统没有特殊的要求&#xff0c;还是安装 Raspberry Pi OS …

uniapp登录页面( 适配:pc、小程序、h5)

<!-- 简洁登录页面 --> <template><view class"login-bg"><image class"img-a" src"https://zhoukaiwen.com/img/loginImg/2.png"></image><image class"img-b" src"https://zhoukaiwen.com/im…

KAN 学习 Day4 —— MultKAN 正向传播代码解读及测试

在KAN学习Day1——模型框架解析及HelloKAN中&#xff0c;我对KAN模型的基本原理进行了简单说明&#xff0c;并将作者团队给出的入门教程hellokan跑了一遍&#xff1b; 在KAN 学习 Day2 —— utils.py及spline.py 代码解读及测试中&#xff0c;我对项目的基本模块代码进行了解释…

『功能项目』怪物的有限状态机【42】

本章项目成果展示 我们打开上一篇41项目优化 - 框架加载资源的项目&#xff0c; 本章要做的事情是按照框架的思想构建项目并完成怪物的自动巡逻状态&#xff0c;当主角靠近怪物时&#xff0c;怪物会朝向主角释放技能 首先新建脚本&#xff1a;BossCtrl.cs (通常把xxxCtrl.cs脚…

SpringBoot2:请求处理原理分析-利用内容协商功能实现接口的两种数据格式(JSON、XML)

文章目录 一、功能说明二、案例实现1、基于请求头实现2、基于请求参数实现 一、功能说明 我们知道&#xff0c;用ResponseBody注解标注的接口&#xff0c;默认返回给页面的是json数据。 其实&#xff0c;也可以返回xml结构的数据给页面。 这一篇就来实现一下这个小功能。 二、…