单链表的介绍和实现

前言

Hello,小伙伴们,你们的作者君又回来了,今天我将带领大家继续学习另一种线性表:单链表,

准备好的小伙伴三连打卡上车,你们·的支持就是我更新的动力,一定不要吝啬手中的三连哟,万分感谢!

1.单链表的介绍

1.1什么是单链表

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

 链表的结构跟火车的车厢相似,淡季时车次很少,相应的车厢也可能会减少,旺季时相应的车厢也会减少。只需要将火车里的某节车厢加上或者是减去就好了,每节车厢都是独立的存在,彼此的增减删除不会影响其他的车厢的运作。

车厢是独立存在的,且每节车厢都是有车门的。想象一下这样的场景。假设每节车厢的车门都是上锁的状态,最简单的做法就是:每节车厢都有下一节车相处的钥匙。

在链表里,每节“车厢”大概是什么样的呢?

与顺序表不同,链表的每节“车厢”都是独立申请下来的空间,我们称之为“节点/结点”;

结点组要有两个部分组成:当前节点需要保存的数据和下一个节点的地址(指针变量)

在图中的指针变plist就是链表的头结点。

为什么我们还需要指针变量来保存下一个节点的位置?

链表中的每一个节点都是独立申请的(即在需要插入数据的时候才需要去申请空间),我们就需要通过指针变量来保存下一个节点的位置才能实现从一个节点找的下一个节点的操作。

结合前面学过的知识,我们用结构体来定义我们需要的节点。

假设我们需要的节点数据为整形:
 

struct SListNode
{
int data; //节点数据
struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};

当我们想要保存一个整形数据时,实际上就是向系统申请了一块空间,这个内存不仅保存了整形数据还保存了下一个节点的地址(当下一个节点为空时保存的址也为空!)

2.链表的实现

和实现顺序表的步骤一样我们还是需要创建3个文件

根据前面篇幅关于链表的介绍,我们可以开始定义链表的结构:

typedef int NodeType

 这一步的操作是为了方便后面的一键替换就和前面的顺序表的实现定义一样,为了方便后面的链表可以轻松的存储其他类型的数据。

既然链表是线性表的一种,那我们也可以向实现顺序表那样来实现链表的增删查改操作:
那首先我们先来实现一下链表的尾插吧

2.1链表的尾插

我们先来看该功能的实现函数的定义:

//实现单链表数据的尾插void ListNodePushBack(ListNode** ps, NodeType x);

再来看看怎样来实现来实现链表的尾插功能:
 

//实现单链表数据的尾插void ListNodePushBack(ListNode** ps, NodeType x)
{ListNode* temp = CreatNode(x);if (*ps == NULL){*ps = temp;}else{ListNode* pcur = *ps;while (pcur->next != NULL){pcur = pcur->next;}pcur->next = temp;}
}

在这里,我们要注意:为什么我们要传递二级指针呢

其实是为了解决*ps为空指针的情况 ,如果传过来的是一个空指针,直接进行寻找链表的最后一个节点的操作就会出现对空指针的解引用,从而引起报错!!!

2.1.1链表空间节点的申请

在介绍之前我们还要清楚 CreatNode函数的作用:

ListNode* CreatNode(NodeType x)
{ListNode* temp = (ListNode*)malloc(sizeof(ListNode));if (temp == NULL){perror("malloc");exit(1);}else{temp->next = NULL;//尾节点的next指针指向NULLtemp->x = x;//让申请下来的空间能存储想要插入的值return temp;}
}

在这里,我们需要清楚,链表相较于顺序表是没有增容的概念的,每一个节点的空间都是相对独立的,除了中间的指针变量作为引线将他们彼此相连,任何一块“车厢的消失”都不会影响到整个“列车”的运行

2.1.2 尾插的进行

想要进行尾插,就一定要找到尾插对象链表的尾节点才能进行下一步操作:

ListNode* pcur = *ps;while (pcur->next != NULL){pcur = pcur->next;}pcur->next = temp;}

从上面的代码可以知道,当pcur->next == NULL时循环跳出,此时pcur也正好指向了链表的尾节点

所以此时只需要pcur->next指向的对象变为刚才申请下来的节点就可以完成尾插的工作,这样新申请下来的空间就可以成为新的尾节点。

尾插的效果演示:

在进行尾插效果的展示时,我们为了能够更加直观的观察到效果,我们可以先来实现单链表的数据打印:

单链表数据的打印(LIstNodePrint函数)

先来看看他的申明:

//打印单链表
void ListNodePrint(ListNode* s);

由于打印的操作并不需要链表的数据有任何改变,所以就只需要传递头结点的指针变量即可:

void ListNodePrint(ListNode* s)
{while (s){printf("%d->", s->x);s = s->next;}printf("NULL");
}
尾插测试: 

此时,我们再来测试我们的代码,看看效果如何: 

 2.2链表的头插

有了尾插的基础,实现头插就要简单一些了

首先我们先看看头插函数的定义:
 

//前插元素
void ListNodePushFront(ListNode** ps, NodeType x);

再看看他的实现代码:

//前插元素
void ListNodePushFront(ListNode** ps, NodeType x)
{assert(ps);ListNode* temp = CreatNode(x);temp->next = *ps;*ps = temp;//在初始的状态下,*ps指向的是原单链表的首节点,进行头插后就成为了第二个节点
//此时需要将temp赋给*ps才能真正的完成头插的操作}

这里的操作就会简单很多,就是需要申请一个新的节点,存放想要头插的数据并使其成为新的首节点。

头插测试:

此时头插传操作完成!! 

2.3链表的尾删

 

 由上图的分析可知:

想要删除最后一个节点,就必须找到最后一个节点和倒数第二个节点

我们先来看看实现该功能的函数的定义

//尾删元素
void ListNodePopBack(ListNode** ps);

函数的实现代码:

//尾删元素
void ListNodePopBack(ListNode** ps)
{assert(ps && *ps);//处理只有一个节点的元素if ((*ps)->next == NULL){free(*ps);*ps = NULL;}else{ListNode* prev = *ps;while (prev->next->next != NULL){prev = prev->next;}free(prev->next);prev->next = NULL;}
}

结合上文的图我们可以知道

prev->next->next == NULL时

就相当于last->next = NULL;

此时prev就是倒数第二个节点

之后就是将最后一个节点释放掉。

尾删测试: 

 2.4链表的头删

我们先来看看头删函数的定义

//头删元素
void ListNodePopFront(ListNode** ps);

再来看看该功能的实现:

//头删元素
void ListNodePopFront(ListNode** ps)
{assert(ps && *ps);ListNode* tem = *ps;*ps = (*ps)->next;//考虑特殊的情况,若链表中只有一个元素,这一步就可以将头结点赋值为NULL,无需进行特殊的处理!!!free(tem);tem = NULL;}

实现了链表的尾删,链表的前删就相较简单一些

这里我们只需要将原链表的第二个节点变为头结点,再将原链表的第一个节点释放了,就能实现单链表的头插功能!!

头删测试:

 接下来我们来实现我们的链表特定数据的查找

2.5链表特定数据的查找

先来看该函数的定义:

我们还是先来看这个函数的定义:

//特定元素的查找
ListNode* LIstNodeFind(ListNode* ps, NodeType x);

再来看看这个函数的实现逻辑:

//特定元素的查找ListNode* LIstNodeFind(ListNode* ps, NodeType x)
{assert(ps);while (ps){if (ps->x == x){return ps;}ps = ps->next;}printf("要查找的数据不存在!!!\n");return NULL;
}

 想要找到特定元素的位置,就需要去遍历整个单链表,当找到是就返回该节点。

查找测试:

2.6在指定位置之前插入节点

我们先来看看这个函数的定义:

//在指定的位置之前插入数据
void ListNodeInsert(ListNode** ps, ListNode* pos, NodeType x);

由此我们可以结合上面实现的制定数据查找的函数使用。

我们来看看他的实现逻辑:

//在指定的位置之前插入数据
void ListNodeInsert(ListNode** ps, ListNode* pos, NodeType x)
{assert(ps && pos );ListNode* pcur = *ps;//当pos == *ps时就相当于头插节点if (pos == *ps){ListNodePushFront(ps, x);}else{while (pcur->next != pos){pcur = pcur->next;}if (pcur == NULL){printf("找不到指定的位置!!\n");return ;}else{ListNode* tem = CreatNode(x);pcur->next = tem;tem->next = pos;}}
}

 在这里我们只需要注意一下特殊的情况:
当*ps == pos是就相当于头插数据,此时就可适当的调用我们已经实现的函数来完成我们代码,减小了代码的工程量。

在指定位置插入节点就相当于如图所示的过程:

指定位置之前插入测试:

 

2.7 在指定位置之后插入节点

首先我们还是先来看该函数的定义:

//在指定的位置之后插入的数据
void ListNodeInsertAfer(ListNode* pos, NodeType x);

这里为什么不在需要头结点的位置呢?

 

有图上可知:

关于指定位置的后插:只与pos节点和pos->next有关,所以就不再需要传输头结点了!! 

我们再来看函数的实现逻辑:

//在指定的位置之后插入的数据
void ListNodeInsertAfer(ListNode* pos, NodeType x)
{assert(pos);ListNode* node = CreatNode(x);ListNode* temp = pos->next;pos->next = node;node->next = temp;}

指定位置数据后插测试:

2.8删除指定的节点

我门先来看看这个函数的声明:

//删除pos节点
void ListNodeErase(ListNode** ps,ListNode* pos);

再来看看这个函数的实现逻辑:

//删除pos节点
void ListNodeErase(ListNode** ps, ListNode* pos)
{assert(ps && pos && *ps);ListNode* pcur = *ps;
//这里相当于头删,可以使用我们之前实现过的头删函数
if(*ps == pos)
{ListNodePopFront(ps);
}else
{while (pcur->next != pos){pcur = pcur->next;}pcur->next = pos->next;free(pos);pos = NULL;
}

其原理就如图所示:

 

删除指定位置的节点测试:

2.9链表的销毁

我们下来看该函数的定义:
 

//销毁该链表:
void ListDestroy(ListNode** ps);

实现该功能的逻辑其实就是遍历链表并将所有的节点都释放掉。
 逻辑的实现:

//销毁该链表:
void ListDestroy(ListNode** ps)
{assert(ps && *ps);ListNode* next = NULL;while (*ps){next = (*ps)->next;free(*ps);*ps = next;}*ps = NULL;
}

销毁测试: 

3.完整代码的展示

3.1 test.c

#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"void Test1()
{ListNode* s = NULL;ListNodePushBack(&s, 1);ListNodePushBack(&s, 2);ListNodePushBack(&s, 3);ListNodePushBack(&s, 4);ListNodePushFront(&s, 0);ListNodePushFront(&s, -1);ListNodePrint(s);ListDestroy(&s);ListNodePrint(s);}
int main()
{Test1();return 0;
}

 3.2 ListNode.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"void DestroyList(ListNode* ps)
{assert(ps);while (ps){ListNode* cur = ps;ps = ps->next;free(cur);} 
}//实现单链表数据的尾插
ListNode* CreatNode(NodeType x)
{ListNode* temp = (ListNode*)malloc(sizeof(ListNode));if (temp == NULL){perror("malloc");exit(1);}else{temp->next = NULL;temp->x = x;return temp;}
}
void ListNodePushBack(ListNode** ps, NodeType x)
{ListNode* temp = CreatNode(x);if (*ps == NULL){*ps = temp;}else{ListNode* pcur = *ps;while (pcur->next != NULL){pcur = pcur->next;}pcur->next = temp;}
}void ListNodePrint(ListNode* s)
{while (s){printf("%d->", s->x);s = s->next;}printf("NULL\n");
}//前插元素
void ListNodePushFront(ListNode** ps, NodeType x)
{assert(ps);ListNode* temp = CreatNode(x);temp->next = *ps;*ps = temp;}//尾删元素
void ListNodePopBack(ListNode** ps)
{assert(ps && *ps);//处理只有一个节点的元素if ((*ps)->next == NULL){free(*ps);*ps = NULL;}else{ListNode* prev = *ps;while (prev->next->next != NULL){prev = prev->next;}free(prev->next);prev->next = NULL;}
}//头删元素
void ListNodePopFront(ListNode** ps)
{assert(ps && *ps);ListNode* tem = *ps;*ps = (*ps)->next;//考虑特殊的情况,若链表中只有一个元素,这一步就可以将头结点赋值为NULL,无需进行特殊的处理!!!free(tem);tem = NULL;}//特定元素的查找ListNode* LIstNodeFind(ListNode* ps, NodeType x)
{assert(ps);while (ps){if (ps->x == x){return ps;}ps = ps->next;}printf("要查找的数据不存在!!!\n");return NULL;
}//在指定的位置之前插入数据
void ListNodeInsert(ListNode** ps, ListNode* pos, NodeType x)
{assert(ps );ListNode* pcur = *ps;//当pos == *ps时就相当于头插节点if (pos == *ps){ListNodePushFront(ps, x);}else{while (pcur->next != pos){pcur = pcur->next;}if (pcur == NULL){printf("找不到指定的位置!!\n");return ;}else{ListNode* tem = CreatNode(x);pcur->next = tem;tem->next = pos;}}
}//在指定的位置之后插入的数据
void ListNodeInsertAfer(ListNode* pos, NodeType x)
{assert(pos);ListNode* node = CreatNode(x);ListNode* temp = pos->next;pos->next = node;node->next = temp;}//删除pos节点
void ListNodeErase(ListNode** ps, ListNode* pos)
{assert(ps && pos && *ps);ListNode* pcur = *ps;//当*ps == pos时,就相当于时删除头结点if (*ps == pos){ListNodePopFront(ps);}else{while (pcur->next != pos){pcur = pcur->next;}pcur->next = pos->next;free(pos);pos = NULL;}
}//销毁该链表:
void ListDestroy(ListNode** ps)
{assert(ps && *ps);ListNode* next = NULL;while (*ps){next = (*ps)->next;free(*ps);*ps = next;}*ps = NULL;
}

3.3 List.h

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int NodeType;
typedef struct ListNode
{NodeType x;struct ListNode* next;
}ListNode;//后面我们也要实现单链表的增删查改!!!//首先实现单链表的初始化
void InitListNode(ListNode* ps);//实现单链表的销毁
void DestroyList(ListNode* ps);//实现单链表数据的尾插void ListNodePushBack(ListNode** ps, NodeType x);//打印单链表
void ListNodePrint(ListNode* s);//前插元素
void ListNodePushFront(ListNode** ps, NodeType x);//尾删元素
void ListNodePopBack(ListNode** ps);
//头删元素
void ListNodePopFront(ListNode** ps);//特定元素的查找
ListNode* LIstNodeFind(ListNode* ps, NodeType x);//在指定的位置之前插入数据
void ListNodeInsert(ListNode** ps, ListNode* pos, NodeType x);//在指定的位置之后插入的数据
void ListNodeInsertAfer(ListNode* pos, NodeType x);//删除pos节点
void ListNodeErase(ListNode** ps,ListNode* pos);//销毁该链表:
void ListDestroy(ListNode** ps);

结语

好,今天的内容就到这里,感谢你的观看,如果喜欢我的内容还给个三连,若是有什么问题,也欢迎大家来评论区理性讨论,咱们下期再见,拜拜!!

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

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

相关文章

ElementUI el-select 组件动态设置disabled后,高度变更的问题解决办法

问题描述 Vue2 项目在使用 el-select 组件时&#xff0c;动态将disabled变更为了 true&#xff0c;元素的高度发生了变化。 问题原因 通过浏览器开发人员工具面板&#xff0c;发现&#xff0c;组件内的 input 元素被动态设置了height的样式&#xff1a; 在项目中检查后并…

深度解析:如何优雅地删除GitHub仓库中的特定commit历史

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

three.js创建基础模型

场景是一个三维空间&#xff0c;是所有物品的容器。可以将其想象成一个空房间&#xff0c;里面可以放置要呈现的物体、相机、光源等。 通过new THREE.Scene()来创建一个新的场景。 /**1. 创建场景 -- 放置物体对象的环境*/ const scene new THREE.Scene();场景只是一个三维的…

django学习入门系列之第四点《案例 后台管理样例》

文章目录 往期回顾 前期准备&#xff1a; 导航新建&#xff0c;按钮表格 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><!-- 开发版本 --><link rel"stylesheet…

2024-07-16 Unity插件 Odin Inspector6 —— Group Attributes

文章目录 1 说明2 Group 特性2.1 BoxGroup2.2 ButtonGroup2.3 FoldoutGroup2.4 ShowIfGroup / HideIfGroup2.5 HorizontalGroup2.6 ResponsiveButtonGroup2.7 TabGroup2.8 ToggleGroup2.9 VerticalGroup 1 说明 ​ 本文介绍 Odin Inspector 插件中有关 Group 特性的使用方法。…

【解决问题】permission denied while trying to connect to the Docker daemon socket

解决方法 sudo usermod -aG docker $USER 运行上面命令&#xff0c;将当前用户添加到 docker 组&#xff0c;重启电脑。 GPT-4o (OpenAI) 看起来你在尝试通过 make build 构建项目时遇到了权限问题&#xff0c;尤其是在拉取 Docker 镜像时没有权限访问 Docker 的 Unix 套接…

如何使用 GPT?

​通过实例&#xff0c;来展示如何最好地使用 GPT。 生成文字 假设你在写一篇文章&#xff0c;需要在结尾加上这样一句&#xff1a;「California’s population is 53 times that of Alaska.」&#xff08;加州的人口是阿拉斯加州的 53 倍&#xff09;。 但现在你不知道这两个…

谷歌准备斥资 230 亿收购网络安全初创公司 Wiz

Alphabet 正在就收购 Wiz 进行深入谈判&#xff0c;这将显著增强其安全能力。这将是谷歌母公司有史以来最大规模的收购。 这是路透社根据匿名消息来源撰写的内容。目标收购金额为230亿美元&#xff0c;即211亿欧元。 Wiz 拥有实时检测和响应网络威胁的技术。通过实施人工智能…

有关电力电子技术的一些相关仿真和分析:⑥单相相控调压电路与单相斩控调压电路(MATLAB/Siumlink仿真)

针对单相相控调压电路&#xff0c;仿真研究对于给定负载&#xff0c;不同触发角作用下&#xff0c;输出电压波形和输入电流波形&#xff08;对照电网电压&#xff09;&#xff0c;研究输出电压有效值随触发角变化的规律&#xff0c;讨论并验证输入电流连续的条件。采用相同的电…

WPF实现一个带旋转动画的菜单栏

WPF实现一个带旋转动画的菜单栏 一、创建WPF项目及文件1、创建项目2、创建文件夹及文件3、添加引用 二、代码实现2.ControlAttachProperty类 一、创建WPF项目及文件 1、创建项目 打开VS2022,创建一个WPF项目&#xff0c;如下所示 2、创建文件夹及文件 创建资源文件夹&…

<Qt> 初识Qt

目录 一、项目文件解析 widget.h main.cpp widget.cpp widget.ui .pro文件 二、QT 实现Hello World程序 &#xff08;一&#xff09;按钮控件 1. 纯代码 2. 图形化 &#xff08;二&#xff09;标签控件 1. 纯代码 2. 图形化 三、内存泄漏问题 四、qdebug()的使用…

php基础: 三角形

包含&#xff1a;左三角、左上三角、右三角、右上三角、等腰三角、倒等腰三角。注意空格的数量&#xff0c;因为*号后面加了空格 /*** * 左三角形* param $n* return void*/ function triangleLeft($n){echo <pre>;for ($i 1; $i < $n; $i) {for ($j 1; $j < $i…

el-table的selection多选表格改为单选

需求场景: 选择表格数据时&#xff0c;需要控制单条数据的操作按钮是否禁用。 效果图: html代码: <div><el-tableref"multipleTable":data"tableData"tooltip-effect"dark"style"width: 100%"selection-change"handl…

Android:创建自定义View

点击查看创建自定义view官网文档 一、简介 设计良好的自定义视图与任何其他精心设计的类一样。它通过一个简单的接口封装一组特定的功能&#xff0c;高效使用 CPU 和内存&#xff0c;诸如此类。除了是一个精心设计的类之外&#xff0c;自定义视图还必须执行以下操作&#xff1…

LinuxShell编程2——shell搭建Discuzz论坛网站

目录 一、环境准备 ①准备一台虚拟机 ②初始化虚拟机 1、关闭防火墙 2、关闭selinux 3、配置yum源 4、修改主机名 二、搭建LAMP环境 ①安装httpd(阿帕奇apache&#xff09;服务器 查看是否安装过httpd 启动httpd 设置开机启动 查看状态 安装网络工具 测试 ②安装…

STM32 - FLASH 笔记

STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分&#xff0c;通过闪存存储器接口&#xff08;外设&#xff09;可以对程序存储器和选项字节进行擦除和编程 读写FLASH的用途&#xff1a; 利用程序存储器的剩余空间来保存掉电不丢失的用户数据 通过在程序…

java设计模式(十五)命令模式(Command Pattern)

1、模式介绍&#xff1a; 命令模式&#xff08;Command Pattern&#xff09;是一种行为设计模式&#xff0c;其主要目的是将请求封装成一个对象&#xff0c;从而允许使用不同的请求、队列或者日志来参数化其他对象。这种模式使得命令的请求者和实现者解耦。 2、应用场景&…

【ARM】MDK-服务器与客户端不同网段内出现卡顿问题

【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 记录不同网段之间的请求发送情况以及MDK网络版license文件内设置的影响。 2、 问题场景 客户使用很久的MDK网络版&#xff0c;在获取授权时都会出现4-7秒的卡顿&#xff0c;无法对keil进行任何操作&#xff0c;彻底…

ChatGPT Mac App 发布!

2024 年 6 月&#xff0c;OpenAI 的大语言模型 ChatGPT 的 Mac 客户端与 ChatGPT-4o 一起发布了。ChatGPT Mac 户端可以让用户直接在 Mac 电脑上使用 ChatGPT 进行对话。它提供了一个简单易用的用户界面&#xff0c;用户可以在其中输入文本或语音指令&#xff0c;并接收模型生成…

【Python百日进阶-Web开发-音频】Day702 - librosa安装及模块一览表

文章目录 一、Librosa简介与安装1.1 Librosa是什么1.2 Librosa官网 二、Librosa安装2.1 安装Librosa 三、安装ffmpeg3.1 ffmpeg官网下载3.2 ffmpeg安装3.2.1 解压3.2.2 添加环境变量3.2.3 测试ffmpeg是否安装成功 四、Librosa 库模块一览4.1 库函数结构4.2 Audio processing&am…