数据结构--链表的基本操作

1. 链表的概念及结构

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

链表也是线性表的一种。

链表的结构跟⽕⻋⻋厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只 需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。

⻋厢是独⽴存在的,且每节⻋厢都有⻋⻔。想象⼀下这样的场景,假设每节⻋厢的⻋⻔都是锁上的状 态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从⻋头⾛到⻋尾?

最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。 

在链表⾥,每节“⻋厢”是什么样的呢?

与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点

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

图中指针变量 plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希 望plist“指向”第⼆个节点时,只需要修改plist保存的内容为0x0012FFA0。

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

链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。

结合前⾯学到的结构体知识,我们可以给出每个节点对应的结构体代码:

假设当前保存的节点为整型:

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

当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数 据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。 当我们想要从第⼀个节点⾛到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下⼀个 节点的钥匙)就可以了。

给定的链表结构中,如何实现节点从头到尾的打印?

思考:当我们想保存的数据类型为字符型、浮点型或者其他⾃定义的类型时,该如何修改?

补充说明:

1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续

2、节点⼀般是从堆上申请的

3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续

2. 单链表的实现

我们和顺序表一样定义三个文件,一个头文件我们取个名字叫SList.h,两个源文件分别取名叫SList.ctext.c,我们的SList.c用来实现链表的方法,text.c用来测试。

我们把单链表的功能函数全部在头文件里面声明一下,以及我们结构体的定义,还有我们需要用到的头文件都放在.h文件里面。

.h

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>//定义节点的结构
typedef int SLTDataType;typedef struct SListNode
{SLTDataType date;//数据struct SListNode* next;//指向节点的地址}SLTNode;void SLTPrint(SLTNode* phead);//打印void SLTBuyBode(SLTNode** pphead, SLTDataType x);//尾插void SLPushFront(SLTNode** pphead, SLTDataType x);//头插void SLTPopBack(SLTNode** pphead);//尾删void SLTPopFront(SLTNode** pphead);//头删SLTNode* SLFind(SLTNode* phead, SLTDataType x);//查找void SLTnsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//指定位置之前插入数据void SLTnesertAfter(SLTNode* pos, SLTDataType x);//指定位置之后插入数据void SLTErase(SLTNode** pphead, SLTNode* pos);//删除pos节点void SLTEreaseAfter(SLTNode* pos);//删除pos之后的节点void SListDesTroy(SLTNode** pphead);//销毁链表

以上就是我们的声明了,要注意我们的类型,我们这里用typedef 定义了一个int类型给它取了个名字叫SLTDataType,当我们需要改类型的时候直接把int改成我们想要的类型就行。


我们和顺序表一样先来实现尾插,我们插入数据的时候需要申请节点,所以我们先来实现申请节点的函数。

节点的申请

SList.c
SLTNode* SLBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->date = x;newnode->next = NULL;return newnode;
}

申请空间可能导致申请失败所以我们需要判断一下,x是我们节点里面的数据。

现在来实现尾插

//尾插
void SLTBuyBode(SLTNode** pphead, SLTDataType x)
{assert(pphead);//*pphead就是指向第一个节点的指针//空链表和非空链表SLTNode* newnode = SLBuyNode(x);if (*pphead == NULL){*pphead = newnode;}else{//找尾SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}//ptail指向的就是尾节点ptail->next = newnode;}
}

如果我们传过来一个NULL,我们可以对他解引用吗?那肯定是不行的,所以我们加个断言。

我们要插入数据那肯定是需要申请节点的,我们把需要插入的数据x传过去,然后进行找尾找到后让我们的尾节点的next指向我们的新节点。

这里我们创建了一个指针变量让它进行找尾的操作,如果用我们的头节点不断往后最后会指向我们的尾节点。

尾插实现好了我们用测试文件来测试一下

text.c

打印怎么实现的我们上面已经说了。我把打印的代码直接放这了(SList.c)

void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->date);pcur = pcur->next;}printf("NULL\n");
}

头插的实现

//头插
void SLPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = SLBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}

头插的实现其实就是把我们申请好的节点让它的next指向我们的头节点。 

测试一下:


尾删的实现


//尾删
void SLTPopBack(SLTNode** pphead)
{assert(pphead && *pphead);//链表只有一个节点if ((*pphead)->next == NULL)//-->优先级*{free(*pphead);*pphead = NULL;}else{//链表有多个节点SLTNode* prev = *pphead; //尾节点的前一个节点SLTNode* ptail = *pphead;//尾节点while (ptail->next){prev = ptail;//尾节点的前一个节点ptail = ptail->next;}free(ptail);ptail = NULL;prev->next = NULL;//让我的尾节点指向空指针}
}

既然我们要删除节点那么节点不能为空,所以需要断言一下。

当链表只有一个节点的时候我们直接把它释放就行了。

有多个节点的时候我们需要用一个指针变量用来保存我们的前一个节点。 

测试一下:


头删的实现

//头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}

我们要把头节点给释放掉,所以我们先把头节点的next指向下一个节点的地址给保存起来。

测试一下:


查找的实现

//查找
SLTNode* SLFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur){if (pcur->date == x){return pcur;}pcur = pcur->next;}return  NULL;
}

如果找到了我们就直接返回找到的节点,没找到返回NULL。

测试一下:


指定位置之前插入数据的实现


//指定位置之前插入数据
void SLTnsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLBuyNode(x);if (pos == *pphead){//如果pos等于我们的头节点我们直接使用头插SLPushFront(pphead, x);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}
}

pos是我们指定的位置,所以不能为空我们断言一下。

插入数据肯定是要申请节点的。

如果pos等于我们的头节点那么我们直接调用头插就可以了。

我们定义一个指针变量用来保存前一个节点,然后用我们申请好了的节点的next指向我们的pos,在用我们前一个节点的next指向我们申请好了的节点。

测试一下:


指定位置之后插入数据

//指定位置之后插入数据
void SLTnesertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}

这里我们用不到头节点,所以只有两个参数,这里的pos是指定位置所以不能为空我们要加个断言,这里直接用申请好的节点的next指向pos的下一个节点,然后再让pos的next指向我们申请好的节点,这样就插入进去了。

 测试:


删除pos节点

/删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);if (pos == *pphead){//如果等于说明要删除的就是头节点,所以我们直接头删SLTPopFront(pphead);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}

删除指定pos节点的实现和我们在指定位置之前插入数据的实现有点相似。

当跳出while循序的时候说明prev->next已经等于pos了,所以我们让perv->next指向pos的下一个节点,然后把pos释放掉。

测试:


删除pos之后的节点

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

删除pos之后的节点,直接把pos指向的下一个节点的地址保存起来,这里面我们定义了一个指针变量用来存放下一个节点,然后把del节点的next指向的节点给pos->next,最后释放掉del。

测试:


销毁链表

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

通过循序对节点一个一个的释放。

最后置为NULL


3.单链表的所有代码

SList.h

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>//定义节点的结构
typedef int SLTDataType;typedef struct SListNode
{SLTDataType date;//数据struct SListNode* next;//指向节点的地址}SLTNode;void SLTPrint(SLTNode* phead);//打印void SLTBuyBode(SLTNode** pphead, SLTDataType x);//尾插void SLPushFront(SLTNode** pphead, SLTDataType x);//头插void SLTPopBack(SLTNode** pphead);//尾删void SLTPopFront(SLTNode** pphead);//头删SLTNode* SLFind(SLTNode* phead, SLTDataType x);//查找void SLTnsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//指定位置之前插入数据void SLTnesertAfter(SLTNode* pos, SLTDataType x);//指定位置之后插入数据void SLTErase(SLTNode** pphead, SLTNode* pos);//删除pos节点void SLTEreaseAfter(SLTNode* pos);//删除pos之后的节点void SListDesTroy(SLTNode** pphead);//销毁链表

SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->date);pcur = pcur->next;}printf("NULL\n");
}//判断是否为非空链表
SLTNode* SLBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->date = x;newnode->next = NULL;return newnode;
}//尾插
void SLTBuyBode(SLTNode** pphead, SLTDataType x)
{assert(pphead);//*pphead就是指向第一个节点的指针//空链表和非空链表SLTNode* newnode = SLBuyNode(x);if (*pphead == NULL){*pphead = newnode;}else{//找尾SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}//ptail指向的就是尾节点ptail->next = newnode;}
}//头插
void SLPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = SLBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}//尾删
void SLTPopBack(SLTNode** pphead)
{assert(pphead && *pphead);//链表只有一个节点if ((*pphead)->next == NULL)//-->优先级*{free(*pphead);*pphead = NULL;}else{//链表有多个节点SLTNode* prev = *pphead; //尾节点的前一个节点SLTNode* ptail = *pphead;//尾节点while (ptail->next){prev = ptail;//尾节点的前一个节点ptail = ptail->next;}free(ptail);ptail = NULL;prev->next = NULL;//让我的尾节点指向空指针}
}
//头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}//查找
SLTNode* SLFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur){if (pcur->date == x){return pcur;}pcur = pcur->next;}return  NULL;
}//指定位置之前插入数据
void SLTnsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLBuyNode(x);if (pos == *pphead){//如果pos等于我们的头节点我们直接使用头插SLPushFront(pphead, x);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}
}//指定位置之后插入数据
void SLTnesertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);if (pos == *pphead){//如果等于说明要删除的就是头节点,所以我们直接头删SLTPopFront(pphead);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}//删除pos之后的节点
void SLTEreaseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}//销毁链表
void SListDesTroy(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}

text.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SListText02()
{SLTNode* plist = NULL;//尾插SLTBuyBode(&plist, 1);SLTBuyBode(&plist, 2);SLTBuyBode(&plist, 3);SLTBuyBode(&plist, 4);SLTPrint(plist);//打印//测试头插SLPushFront(&plist, 0);SLTPrint(plist);//测试尾删SLTPopBack(&plist);SLTPrint(plist);//测试头删SLTPopFront(&plist);SLTPrint(plist);//测试查找SLTNode* find = SLFind(plist,1);if (find == NULL){printf("没找到\n");}else{printf("找到了\n");}//指定位置之前插入数据SLTnsert(&plist,find,6);SLTPrint(plist);//指定位置之后插入数据SLTnesertAfter(find, 7);SLTPrint(plist);//删除pos节点SLTErase(&plist,find);SLTPrint(plist);//删除pos之后的节点SLTEreaseAfter(find);SLTPrint(plist);//销毁链表SListDesTroy(&plist);
}
int main()
{SListText02();return 0;
}

以上就是单链表的基本操作了,如有表述不准确或理解不深刻的地方,还请各位看客不吝指正。

 

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

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

相关文章

嵌入式—STC芯片开发板点亮第一盏灯

&#x1f3ac; 秋野酱&#xff1a;《个人主页》 &#x1f525; 个人专栏:《Java专栏》《Python专栏》 ⛺️心若有所向往,何惧道阻且长 文章目录 一&#xff1a; 原理图二&#xff1a; 需求实现三&#xff1a;编码实现四&#xff1a;代码实现五&#xff1a;编译烧录运行 一&…

HTML哆啦A梦

目录 写在前面 HTML简介 完整代码 代码分析 系列推荐 写在最后 写在前面 谁不想拥有一只可爱的叮当猫呢&#xff1f;本期小编给大家带来了一个萌萌的哆啦A梦。 HTML简介 HTML&#xff0c;即超文本标记语言&#xff0c;是构建网页的基础技术之一&#xff0c;它是一种标…

JavaEE初阶-多线程进阶2

文章目录 前言一、CAS1.1 CAS的概念1.2 原子类1.3 CAS的ABA问题 二、JUC中常用类2.1 Callable接口2.2 ReentrantLock&#xff08;可重入&#xff09;2.3 Semaphore信号量2.4 CountDownLatch类2.5 CopyOnWriteArrayList类2.6 ConcurrentHashMap 前言 对于多线程进阶的部分&…

C语言例题43、打印倒立金字塔

#include <stdio.h>void main() {int i, j;for (i 5; i > 0; i--) {for (j 5; j > i; j--) {//输出空格printf(" ");}for (j 2 * i; j > 1; j--) {//输出星号printf("* ");}printf("\n");} }运行结果&#xff1a; 本章C语言…

用好 explain 妈妈再也不用担心我的 SQL 慢了

大家好&#xff0c;我是聪&#xff0c;一个乐于分享的小小程序员。在不久之前我写了一个慢 SQL 分析工具&#xff0c;可以用来分析 Java Mybatis 项目的 SQL 执行情况&#xff0c;其中刚好涉及到了 explain 的使用。感兴趣的可以了解一下。 Github 地址⭐&#xff1a;https://…

【C#】学习获取程序执行路径,Gemini 帮助分析

一、前言&#xff1a; 在Delphi中&#xff0c;如果想要获取当前执行程序的目录&#xff0c;程序代码如下&#xff1a; ExtractFilePath(ParamStr(0)); 今天在分析一个别人做的C#程序时看到了一段C#代码&#xff0c;意思是获取执行程序所在的文件目录&#xff1a; public stat…

基于区块链的Web 3.0关键技术研讨会顺利召开

基于区块链的Web3.0关键技术研讨会 2024年4月23日&#xff0c;由国家区块链技术创新中心主办的“基于区块链的web3.0关键技术研讨会”召开。Web3.0被用来描述一个运行在“区块链”技术之上的“去中心化”的互联网&#xff0c;该网络上的主体掌握自己数据所有权和使用权&#xf…

【回眸】git VS repo 区别

git VS repo 区别 1. git&#xff1a;Git是一个开源的分布式版本控制系统&#xff0c;用以有效、高速的处理从很小到非常大的项目版本管理。 2. Repo: Repo是谷歌用Python脚本写的调用git的一个脚本,Repo实现管理多个git库。 Git 常用命令 1. git init&#xff1a;在当前目…

【原创】java+springboot+mysql企业邮件管理系统设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

Vue的学习 —— <vue组件>

目录 前言 正文 一、选项式API与组合式API 二、生命周期函数 1、onBeforeMount() 2、onMounted() 3、onBeforeUpdate() 4、onUpdated() 5、onBeforeUnmount() 6、onUnmounted() 三、组件之间的样式冲突 四、父组件向子组件传递数据 1、定义props 2、静态绑定props…

C++青少年简明教程:赋值语句

C青少年简明教程&#xff1a;赋值语句 赋值语句是编程中最基本也是最常用的概念之一&#xff0c;它用于将一个值分配给一个变量。 使用等号&#xff08; 称为赋值运算符&#xff09;来给变量赋值&#xff0c;赋值语句的左边是要赋值的变量&#xff0c;右边是要赋给变量的值。C…

Docker 使用 CentOS 镜像

使用 docker run 直接运行 CentOS 7 镜像&#xff0c;并登录 bash。 C:\Users\yhu>docker run -it centos:centos7 bash Unable to find image centos:centos7 locally centos7: Pulling from library/centos 2d473b07cdd5: Pull complete Digest: sha256:be65f488b7764ad36…

GPT-4o:全面深入了解 OpenAI 的 GPT-4o

GPT-4o&#xff1a;全面深入了解 OpenAI 的 GPT-4o 关于 GPT-4o 的所有信息ChatGPT 增强的用户体验改进的多语言和音频功能GPT-4o 优于 Whisper-v3M3Exam 基准测试中的表现 GPT-4o 的起源追踪语言模型的演变GPT 谱系&#xff1a;人工智能语言的开拓者多模式飞跃&#xff1a;超越…

连接虚拟机的 redis

用Windows 的 Redis Insight 连接虚拟机的 安装redis发现连不上 我的redis是新安装&#xff0c;没有用户名密码&#xff0c;发现是ip问题 127 开头的被我注释了&#xff0c;换成了ifconfig查到的ip

Android性能:SurfaceFlinger与BufferQueue(3)

Android性能&#xff1a;SurfaceFlinger与BufferQueue&#xff08;3&#xff09; Android显示系统的组成可以概括为两大部分&#xff1a;绘制(DrawFrame)合成&#xff08;SurfaceFlinger HWC&#xff09; 绘制&#xff1a;Surface中空的 GraphicBuffer->CPU或者GPU通过Canv…

Python GUI开发- Qt Designer环境搭建

前言 Qt Designer是PyQt5 程序UI界面的实现工具&#xff0c;使用 Qt Designer 可以拖拽、点击完成GUI界面设计&#xff0c;并且设计完成的 .ui 程序可以转换成 .py 文件供 python 程序调用 环境准备 使用pip安装 pip install pyqt5-toolsQt Designer 环境搭建 在pip安装包…

基于SVPWM的飞轮控制系统的simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 基于SVPWM的飞轮控制系统的simulink建模与仿真。SVPWM的核心思想是将逆变器输出的三相电压矢量在两相静止坐标系&#xff08;αβ坐标系&#xff09;中表示&#xff0c;通过控…

Node.js安装及环境配置(超详细!保姆级!!)

目录 一、进入官网地址下载安装包 二、安装程序 三、环境配置 四、测试 五、安装淘宝镜像 一、进入官网地址下载安装包 Node.js — Download Node.js (nodejs.org) 选择对应你系统的 node.js 版本&#xff0c;我选择的是Windows系统&#xff0c;64位 点击图中选项&#…

无人机+应急通信:灾害现场应急通信车技术详解

无人机和应急通信车是灾害现场应急通信中的重要技术。无人机可以通过快速到达灾害现场&#xff0c;搭载高清摄像头、红外热成像仪、激光雷达等设备&#xff0c;对灾区进行实时监测和灾情评估&#xff0c;同时也可以通过搭载的通信设备&#xff0c;与指挥中心进行实时通信和数据…

202009青少年软件编程(Python)等级考试试卷(三级)

第 1 题 【单选题】 通过算式123122021120可将二进制1101 转为十进制,下列进制转换结果正确的是?( ) A :0b10转为十进制,结果是2 B :0d10转为十进制,结果是8 C :0x10转为十进制,结果是10 D :0o10转为十进制,结果是16 正确答案:A 试题解析: 第 2 题 【单选题】 语句flo…