详解单链表(内有精美图示哦)

article/2025/9/20 9:48:15

全文目录

  • 引言
  • 链表
    • 链表的定义与结构
    • 链表的分类
  • 单链表的实现及对数据的操作
    • 单链表的创建与销毁
      • 创建
      • 销毁
    • 单链表的打印
    • 单链表的头插与头删
      • 头插
      • 头删
    • 单链表的尾插与尾删
      • 尾插
      • 尾删
    • 单链表的查找
    • 单链表在pos位置后插入/删除
      • 插入
      • 删除
    • 单链表在pos位置插入/删除
      • 插入
      • 删除
  • 总结

引言

在上一篇文章中,我们了解了顺序表的相关知识,并且实现了用顺序表管理数据。

但在这过程中,我们发现了使用顺序表管理数据时,其实是存在一些不方便的:
比如当存储空间已经被扩容的时候,删除了许多的数据,就会导致大片的内存浪费;
比如当我们需要扩容时,可能会异地扩容,这个过程会比较影响效率;
再比如当我们需要在顺序表前面插入数据时,过程会比较麻烦。

相对于顺序表,同属线性表的链表在这些方面就有着比较好的表现。

链表也有许多不同的种类:单向或双向链表、带头或不带头的链表、循环或非循环的链表等。在接下来的几篇篇文章中就会详细介绍链表的相关知识:

链表

链表的定义与结构

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

也就是说,链表是逻辑上连续,但存储结构上不连续的线性结构。我们可以通过指针访问到链表中的下一个元素。所以,在一个链表的结点中,应该至少包含两个元素:当前结点的数据与指向下一个结点的指针。

这就需要我们用到结构体的知识:我们在学习结构体时介绍过结构体的自引用,即结构体中的一个成员类型是结构体指针。即,我们可以将链表结点的类型定义为(当然,这是最简单的形式,只能依次访问链表的元素):

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

链表的分类

链表有许多的类型:带头与不带头、循环与非循环、单向与双向:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当然,这些种类之间有许多的组合方式,根据需要,可以定义各种各样的链表。

其中,最为简单的就是无头单向非循环链表,这也是此篇文章介绍的重点:

单链表的实现及对数据的操作

对于单链表结点的结构,与上面我们介绍的栗子相同,就是最为简单的类型:
包括当前结点的数据与指向下一个节点的结构体指针:

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

与学习顺序表时类似,我们可以实现一下用单链表来管理数据。同样的,这些功能的实现都将封装为函数:

在这之前,我们首先需要定义一个结构体指针plist,用于访问单链表的第一个结点。这个指针被初始化为NULL:

SListNode* plist = NULL;

单链表的创建与销毁

创建

在开辟顺序表的空间时,我们可以直接动态申请一块连续的空间来存放一些数据。但是对于单链表而言,它在物理空间上不是连续的。所以当我们为单链表开辟空间时,就需要一个结点一个结点分别开辟空间。

首先我们需要一块大小为结构体大小的空间,这块空间可以动态开辟:

SListNode* plist = (SListNode*)malloc(sizeof(SListNode));

在开辟某一个空间后,我们需要将这块空间初始化:将该结点的data成员初始化为想要存储的数据(这个数据可以作为参数传给函数),然后将这个结点的next成员初始化为NULL。

最后,返回已经成功创建的结点的结构体指针:

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{SListNode* plist = (SListNode*)malloc(sizeof(SListNode));if (plist == NULL){perror("malloc");return NULL;}plist->data = x;plist->next = NULL;return plist;
}

单链表的结点是动态开辟的,当然需要将这些空间依次释放掉,以免出现内存泄漏的问题。

销毁

当依次销毁单链表的每一个节点时,我们需要两个指针变量cur与aftercur。通过这两个指针变量,aftercur可以在cur被销毁前记录cur->next的值,从而实现在销毁cur指向的空间后,可以通过aftercur访问到下一个空间而继续进行销毁操作。
依次循环,当cur为NULL时终止,实现销毁每一个结点:
在这里插入图片描述

// 单链表的销毁
void SListDestroy(SListNode* plist)
{SListNode* cur = plist;SListNode* aftercur = cur->next;while (cur){aftercur = cur->next;free(cur);cur = aftercur;}
}

单链表的打印

打印单链表时, 我们只需要遍历单链表,并逐个打印每个结点的data成员即可。

需要注意的是,我们在遍历时,条件必须为cur,而不是cur->next。因为当cur->next为NULL时,cur是最后一个结点。此时,最后一个结点不进入循环,该结点的数据也不会被打印:

// 单链表打印
void SListPrint(SListNode* plist)
{SListNode* cur = plist;while (cur){printf("%d ", cur->data);cur = cur->next;}
}

单链表的头插与头删

头插

在顺序表中,想要从顺序表的前面插入数据是比较麻烦的,这需要将顺序表中的元素整体向后移动一个元素,而获得存放新与元素的空间;

但是在单链表中,头插的实现就比较简单,只需要将新创建的结点接入到单链表的前面即可。我们可以通过让新结点的next成员指向原plist,plist的值指向新结点的方式来实现:
在这里插入图片描述
需要注意的是:
在头插时,是需要改变结构体指针plist的值的,所以我们在传入该结构体指针时,需要传该结构体指针的地址,即二级指针。这样,才能实现将plist的值真正的改变:

当然,当单链表中没有元素时,即plist为NULL时,只需要改变plist的值即可。

void SListPushFront(SListNode** pplist, SLTDateType x)
{SListNode* newnode = BuySListNode(x);if (*pplist == NULL){*pplist = newnode;}else{newnode->next = *pplist;*pplist = newnode;}
}

头删

在顺序表中实现从前面删除也比较麻烦,需要将顺序表中的元素整体向前移动一个数据;

而单链表中只需要使plist指针指向链表中第一个结点的next成员即可。我们可以通过用一个结构体指针cur来记录plist的值,当plist指向下一个结点后,再释放cur指向的空间,即原第一个结点的空间:
在这里插入图片描述

同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。

当然,当plist为空指针时,当然是不能再删除的,所以我们可以assert断言一下。

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

单链表的尾插与尾删

尾插

单链表需要在末尾插入数据时,首先需要找到单链表末尾的位置,然后将单链表最后一个结点的next成员改为新结点的地址即可。
在这里插入图片描述
在找最后一个元素时,我们可以通过cur指针向后移动,直到cur指向的结构体的next成员为NULL时,即cur指向的结点就是单链表的最后一个结点:
在这里插入图片描述
当然,当单链表中没有元素时,即plist为NULL时,只需要改变plist的值即可。
同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。

// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{SListNode* newnode = BuySListNode(x);if (*pplist == NULL){*pplist = newnode;}else{SListNode* cur = *pplist;while (cur->next){cur = cur->next;}cur->next = newnode;}
}

尾删

单链表删除末尾的数据时,同样的,我们需要找到单链表末尾的位置。

然后将单链表中倒数第二个结点的next成员改为NULL,然后释放cur(最后一个结点的指针)指向的空间。
我们可以通过创建一个beforecur变量来存储cur前一个结点的地址,这样,就可以实现当cur指向最后一个元素时,beforecur为倒数第二个元素:
在这里插入图片描述
当单链表中只有一个元素时,cur->next的值本身就是NULL。此时只需要释放cur指向的空间(第一个结点),然后将plist的值改为NULL即可。

同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。
当然,当plist为空指针时,当然是不能再删除的,所以我们可以assert断言一下。

// 单链表的尾删
void SListPopBack(SListNode** pplist)
{assert(*pplist);SListNode* cur = *pplist;SListNode* beforecur = NULL;while (cur->next){beforecur = cur;cur = cur->next;}if (beforecur==NULL){free(*pplist);*pplist = NULL;}else{free(beforecur->next);beforecur->next = NULL;}
}

单链表的查找

之后,我们就会想到要删除单链表中指定的结点。
在删除指定的结点之前,我们首先需要实现一个算法,通过结点中的data成员找到这个节点的位置。并返回这个节点的指针。

遍历单链表,只需要将结构体指针cur依次后移即可。当cur->data的值为x时,返回cur。

// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{SListNode* cur = plist;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

单链表在pos位置后插入/删除

在获取到了pos后,我们就会想要实现在pos位置进行插入或删除:

插入

在插入时,我们很容易想到将新节点newnode->next的值改为pos->next,然后将pos->next的值改为newnode。从而实现将pos位置插入数据:
在这里插入图片描述
显然,这样的算法只能实现在pos后增加结点,想要在pos位置增加结点,这样的条件显然是不足的。

所以我们就先来实现一下在pos后增加数据:

// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{SListNode* newnode = BuySListNode(x);newnode->next = pos->next;pos->next = newnode;
}

删除

在实现删除pos位置的结点时,我们会想到将pos->next的值改为pos->next->next位置的值,然后再释放pos后面的一块空间。
我们可以使用一个afterpos指针来暂存pos->next的值,以方便释放空间:
在这里插入图片描述
同样的,我们发现,这样删除只能释放pos后的一块空间。想要删除pos位置的结点,这样的条件显然是不足的。

但我们可以先实现一下这个函数:

// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{SListNode* afterpos = pos->next;pos->next = afterpos->next;free(afterpos);afterpos = NULL;
}

单链表在pos位置插入/删除

要想在pos位置插入结点,或删除pos位置的结点,我们需要获取到pos结点前面的结点的指针,然后改变pos前面结点中的next成员,由此实现对pos位置数据的操作。

所以在传参的时候,我们需要将单链表第一个结点的地址传给函数,将第一个结点的地址向后遍历得到pos前一个结点的地址后,再进行操作。

我们可以定义一个beforepos指针:

在这里插入图片描述

插入

在获取到beforepos后,我们就可以重复上面的操作来实现在pos位置添加一个新结点:将新节点newnode->next的值改为beforepos->next,然后将beforepos->next的值改为newnode。从而实现在pos位置插入数据:
在这里插入图片描述
但是,在pos位置插入时是有特例的,即pos为单链表的第一个元素时,需要将plist的值改为plist,再将newnode->next的值改为pos即可:
在这里插入图片描述

// 单链表在pos位置插入x
void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x)
{SListNode* newnode = BuySListNode(x);SListNode* beforepos = *pplist;if (beforepos == pos){*pplist = newnode;newnode->next = pos;}else{while (beforepos->next){if (beforepos->next == pos){beforepos->next = newnode;newnode->next = pos;break;}beforepos = beforepos->next;}}
}

删除

当我们得到pos前的结点的地址后,就可以通过相同的方式实现删除pos位置的值:将beforepos->next的值改为beforepos->next位置的值,然后再释放pos后面的一块空间:
在这里插入图片描述

但是,有一种特例,即pos指向的是单链表的第一个元素时,我们只需要将plist的值改为pos->next;再将pos指向的空间释放即可:
在这里插入图片描述

// 单链表删除pos位置的值
void SListErase(SListNode** pplist, SListNode* pos)
{SListNode* beforepos = *pplist;if (beforepos == pos){*pplist = pos->next;free(pos);pos = NULL;}else{while (beforepos->next){if (beforepos->next == pos){beforepos->next = pos->next;free(pos);pos = NULL;break;}beforepos = beforepos->next;}}
}

总结

到此,关于单链表的相关知识就介绍完毕了。当然,单链表是最简单的一种链表类型,在后面我们还会介绍一种比较复杂的链表,即带头双向循环链表。
当然,在介绍带头双向循环链表之前,我会先用一篇文章来讲解一些单链表的题目,欢迎大家持续关注哦

如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出

如果本文对你有帮助,希望一键三连哦

希望与大家共同进步哦


http://chatgpt.dhexx.cn/article/SItjXgBa.shtml

相关文章

【数据结构】单链表(超全)

目录 一、什么是链表?1.1 定义1.2 链表的分类 二、无头单向非循环链表2.1 结构2.2 如何遍历链表数据2.3 尾插2.4 创建新节点2.5 头插2.6 尾删2.7 头删2.8 单链表查找2.9 在pos位置之前插入2.10 删除pos位置数据2.11 在pos位置的后面插入2.12 删除pos位置后面的数据 …

数据结构之单链表的插入

单链表的完整的代码在这篇文章下面,链接: https://blog.csdn.net/six_teen/article/details/113253545 什么是链表? 如上图,链表和生活中的链条很相似,链表在逻辑结构上是以链条的形式连接而成的,但物理结构…

单链表的定义,插入与删除,查找,建立。

链表分为:单链表,双链表,循环链表,静态链表 一,单链表的定义 在内存空间中,各个节点在逻辑上相邻,但在物理上不相邻。 在单个的结点内部需要存放 数据域 和 指针域(存放指向下一个…

单链表的定义

前言 在前面的文章中,我们系统的介绍了线性表的顺序存储实现——顺序表。紧接着我们要介绍线性表的链式存储实现——链表。而链表中又有许多的链表: 单链表双链表循环链表静态链表 这一篇文章中,我们先来介绍单链表。 单链表的定义 什么…

(c语言)详解单链表

1:什么是单链表。 我们知道顺序表底层原理其实就是一块可以自由控制大小的数组,顺序表可以实现在任何地方进行插入一个数据,如果顺序表的缺点在于如果要在起始位置插入一个数据就要把后面的每一个数据都往后挪,这样会大量消耗我们…

双向链表中插入元素的几种方式

dnode的结构如下:由前驱prior指针、后继next指针以及数据data,现需要在A、B节点中间插入C节点,给出了A的地址,以及C的地址。 1、利用将链表拆分然后插入方式进行: 先将节点C完全插入到B的前面,再将A指向C&…

单链表的使用方法.数据结构(三)[上]

前言 提示:文本为数据解构(三)单链表: 本文具体讲解单链表的具体使用方法 提示:以下是本篇文 系列文章目录 第一章 数据解构(一) 第二章 顺序表的具体使用方法.数据解构(二) 文章目录 前言 系列文章目录 文章目录 一、单链表视图 二、…

Linux设置ssh免密登录

目录 1.在/root目录下输入命令 2.进入.ssh目录 3.将公钥id_rsa.pub写入到一个认证文件夹中 4.开启远程免密登录配置 5.免密远程登录本机 1.在/root目录下输入命令 [rootlocalhost ~]# ssh-keygen -t rsa -P "" Generating public/private rsa key pair. Enter …

Linux SSH 免密登录

Linux SSH 免密登录 本篇我们来 看看 Linux 的免密登录的原理 以及实际操作一番 概述 什么是 Linux SSH 免密登录,我觉得大家应该都 多少听过 或者操作过,那你真的理解整个免密登录的过程吗? Linux SSH 免密登录 就是 可以不输入密码 就可以…

SSH登录和SSH免密登录

在了解ssh的时候产生了概念混淆,发现ssh登录和ssh免密登录是两码事。 可以从目的和过程对比这两个概念: 1.目的 1.1 SSH登录 简单来说就是:建立客户端和服务器之间安全的远程连接,登录远程服务器,以访问文件系统 。…

VSCode——SSH免密登录

文章目录 本地PC端(一般为Windows)1. 检查自己是否已经生成公钥2. 配置VScode的SSH config 远程服务器端1. 服务器新建授权文件2. 赋权限3. 重启远程服务器的ssh服务 最全步骤:【设置ssh免密不起作用?彻底搞懂密钥】vscode在remot…

SSH免密登录功能配置

文章目录 一、SSH免密登录功能配置(1)master虚拟机免密登录master虚拟机(2)将公钥拷贝到所创建的虚拟机上 一、SSH免密登录功能配置 ssh密钥登录比密码登录安全,主要是因为他使用了非对称加密,登录过程中需…

CentOS开启SSH免密登录

CentOS开启SSH免密登录 要实现SSH免密登录,首先需要准备一组公钥和私钥。将公钥放到服务器上,将私钥放到客户机上。当客户机连接服务器时,服务器会根据自身的公钥校验客户机的私钥,如果校验通过则允许连接。 一、创建密钥 在客…

Ubuntu开启SSH免密登录

Ubuntu开启SSH免密登录 要实现SSH免密登录,首先需要准备一组公钥和私钥。将公钥放到服务器上,将私钥放到客户机上。当客户机连接服务器时,服务器会根据自身的公钥校验客户机的私钥,如果校验通过则允许连接。 一、创建密钥 在客…

git SSH免密登录

git系列文章目录 第八章 git SSH免密登录的使用 文章目录 git系列文章目录前言一、生成密钥二、使用步骤1.使用VSCODE打开.pub文件复制其中的内容2.打开github或者gitee进入设置选项设置密钥3.使用密钥 总结 前言 虽然Windows系统提供了凭据功能,但是介绍ssh提交&a…

配置SSH免密登录

配置SSH免密登录 1. 在各个虚拟机(master、s1、s2)家目录执行ssh-keygen -b 1024 -t rsa,输入2次回车,输入y/yes再继续回车 ssh-keygen -b 1024 -t rsa 2. 进入到.ssh目录中 ls -all #查看所有文件和文件夹 cd .ssh 查看目录 ls…

SSH免密登录原理

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 SSH免密登录原理 前言一、配置ssh二、无密钥配置1、免密登录原理2、生成公钥和私钥3、将公钥拷贝到要免密登录的目标机器上 三、.ssh文件夹下(~/.ssh)…

ssh免密登录

两台设备,第一台设备作为客户端,第二台设备作为服务端,在第一台使用一个用户免密登录第二台设备。 在第一台设备输入命令: ssh-keygen -t rsa -b 2048 输入命令执行后遇到选项可以选择默认,敲Enter继续执行便可 然后把…

VSCode SSH 免密登录

前提 VSCode 已经安装 Remote - SSH 插件,并且可以通过密码登录远程主机 步骤 假设 VSCode 运行在 Windows,SSH 远程登录 Linux 1、在 Windows 端生成公钥/私钥对 例如在 git bash 中运行 ssh-keygen,然后一路回车,直到出现下面…

SSH免密登录配置

免密登录命令: 1.进入.ssh目录: cd ~/.ssh 2.生成一对密钥: ssh-keygen -t rsa 3.发送公钥: ssh-copy-id 192.168.xx.xxx 4.免密登录测试: ssh 192.168.xx.xxx 目录 一、免密登录原理 二、配置ssh 1.查看 .…