不懂单链表? 这篇文章就帮你搞明白

news/2024/4/24 22:29:07/文章来源:https://blog.csdn.net/carry_carial/article/details/127824172

坚持看完,结尾有思维导图总结

链表对指针的操作要求不低

  • 链表的概念
  • 链表的特性
  • 链表的功能(最重要)
    • 定义和初始化
    • 头插头删
      • 细节说明
    • 尾插尾删
    • 寻找链表元素与打印链表
    • 在 某位置后插入删除
    • 在某位置的插入删除
    • 销毁链表

链表的概念

什么是链表

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

解释一下
通俗的链表解释是 通过箭头链接的
在这里插入图片描述就如同锁链一样链接起来

两个节点之间的空间都没有任何联系
比如 节点A 和节点 B的空间 本来没有任何联系

但是两者通过 next 的指针

从节点 A 能够访问到节点 B ,然后能够一直向下访问知道 空指针

但是实际上
那个箭头是不存在的,是用来理解用的

实际存在的只有地址,访问过程中调用的也是指针(地址)
在这里插入图片描述
从图片的分析来看
一个节点要被分成两半
一般用来储存数据,一般用来储存下一个节点的地址
所以我们程序上的指针的定义可以这样写

typedef int Datatype;
typedef struct SingleList
{Datatype Data;struct SingleList* next;
}SLTNode;

但是下面定义是不对的

typedef int Datatype;
typedef struct SLTNode
{Datatype data;SLTNode* next;
}SLTNode;

因为在内容上 next 的定义在 SLTNode 的 typedef 重命名前,语法错误

链表的特性

链表有什么独特的地方?

所谓独特,必须有所对比,这里取顺序表进行对比

不同点链表顺序表
存储空间物理上空间连续逻辑连续,物理上不一定连续
随机访问可以使用下标直接访问必须遍历找到对应地址
任意位置插入(重要)需要搬移数据只需要修改指向
容量容量不足的时候需要扩容不需要扩容
使用场景元素频繁访问任意位置的插入和删除

所以链表的优点

  1. 插入删除很高效
  2. 寻找元素速度慢
  3. 空间利用率高,不需要扩容

链表的功能(最重要)

我们如何利用链表存储数据和调用数据?

链表本身就是一个数据结构,就需要定义定义一个数据结构,并且实例化
数据结构用来存放取出数据,访问数据
最终要销毁数据结构
所以有如下要求

定义和初始化

第一个问题,链表是由什么组成的?
是由 内容为存储的数据 和 指向下一个节点的 节点组成
第1. 1 的问题是 节点的类型 如何定义?
在这里插入图片描述
用程序的话说就是:

typedef int STDatatype;struct ListNode
{STDatatype data;struct ListNode* next;
};
typedef struct ListNode SLTNode;

但是只有一个节点的结构是不足的
就像只知道有 int 这个类型但是没有数据一样,光有空壳但是没有血肉
如何在 Node 里面填充血肉?
第1.2 个问题:如何创建节点
主要步骤是:

  1. 开辟一个节点空间
  2. 在节点空间中存放数据

对于程序来说就是:

SLTNode* BuyNewNode(Datatype x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if(newnode == NULL){perror("malloc failed");exit(-1);//故障退出}newnode->next = NULL;newnode->data = x;return newnode;
}

第1.3 个问题: 如何初始化一个链表

我们使用单链表的时候,只要拿到链表的头就能够操作了
所以因为头的特殊性,我们单独定义一个名字

typedef SLTNode SLTHead;

随后进行链表的创建
步骤

  1. 循环创建节点
  2. 将节点链接起来

细节

  1. 当创建第一个节点时,头结点改变指向第一个节点,之后的节点头结点位置不再改变
//创建 n 个节点
SLTHead* BuildList(int n )
{SLTHead* phead = NULL;SLTNode* pcur = phead;for(int i = 0;i<n;i++){SLTNode* newnode = BuyNewNode(i);if(phead == NULL){phead = newnode;pcur = phead;}else{pcur->next = newnode;pcur = newnode;}}return phead;
}

我们创建链表的工作就完成了,我们可以调用一下

void Test0()
{//创建一个有10个节点的链表,保留他的头节点SLTHead* pList = BuildList(10);
}

头插头删

链表有两端,头为一端,尾为一端
头尾的图
在这里插入图片描述
要实现头部的插入删除,我们要解决以下问题
第1.1个小方面,头插
第1.1.1个问题
头插的过程是什么样的?
步骤

  1. 生成一个新节点newnode
  2. 这个新节点链接到原来的链表
  3. 链表头向前移动
    在这里插入图片描述
    程序

第一种程序

void SLTPushFront(SLTHead* phead,Datatype x)
{SLTNode* newhead = 	BuyNewNode(x);newnode->next = phead;phead = newnode;	
}

这个程序是错误的,哪里错了呢?
(这个图片需要放大一点看)
在这里插入图片描述
调用函数,创建栈帧的时候
开辟空间把实参的值传给形参
最终函数调用完成后会释放栈帧,销毁变量
如果要改变主函数的类型,就需要传递对应的类型的指针
因为我们要改变的是 头节点的地址,就必须要传形参为指针的地址

正确的程序:

void SLTPushFront(SLTHead** pphead,Datatype x)
{assert(pphead);SLTNode* newnode = BuyNewNode(x);//没有必要对 pcur 判空SLTNode* pcur = *pphead;newnode->next = pcur;*pphead = newnode;
}

我们可以看到,即使链表本来为 null 我们也可以直接进行头插,不需要特殊处理

第1.2个方面,头删
第1.2.个问题
链表的头删是如何工作的?
步骤

  1. 找到头节点的下一个节点
  2. 释放头结点
  3. 令头结点为下一个节点

图解:
在这里插入图片描述
第1.2.2个问题
如果链表被删除到空,该做些什么?
如果是 链表已经为 空,那不能再删除,我们可以使用assert来截断
程序

void SLTPopFront(SLTHead** pphead)
{assert(pphead);assert(*pphead);SLTNode* pcur = *pphead;SLTNode* plast = pcur;pcur = pcur->next;free(plast);*pphead = pcur;
}

难点:
对头指针的改变需要传递指针的地址

细节说明

assert的使用
为什么使用assert ?
因为,在assert报错的时候,程序会定位到报错的位置
在调试的时候出乎意料地节省很多很多时间
(真地非常好用)

尾插尾删

第1.1 个问题,尾插的步骤是什么样子的?

  1. 找到尾
  2. 向尾部插入数据
    图解:
    在这里插入图片描述

第1.2 个问题,尾插的难点是什么?
1 如果链表本身没有元素
如果没有元素,就需要改变主函数中的头结点指针,因为要改变头结点指针,所以要传指针的地址
2 找到尾巴的过程
依次遍历,直到 next 为空

程序:

void SLTPushBack(SLTHead** pphead,Datatype x)
{assert(pphead);SLTNode* pcur = *pphead;SLTNode* newnode = BuyNewNode(x);if(pcur == NULL){*pphead = newnode;return;}while(pcur->next){pcur = pcur->next;}pcur->next = newnode;
}

第2.1 个问题,尾删的步骤是什么样子的?
步骤
1 找到尾
2 然后把尾巴删除

第2.2个问题,尾删的难点是什么?
1 上一个节点如果的 next 如果置为空,就无法找到下一个节点,找到下一个节点就无法回溯到上一个节点,所以需要保存上一个节点的位置,所以需要 plast
图解
在这里插入图片描述
2 同时,如果原来链表是空,就不能再进行删除
3 如果只有一个元素,头结点就需要置空
程序

void SLTPopBack(SLTHead** pphead)
{assert(pphead);assert(*pphead);SLTNode* pcur = *pphead;SLTNode* plast = *pphead;while(pcur->next != NULL){pcur = pcur->next;plast = pcur;}//判断是否只剩下一个节点if(pcur == plast){free(pcur);*pphead = NULL;}else{free(pcur);plast->next = NULL;}
}

寻找链表元素与打印链表

第1.1个问题寻找链表元素需要做什么?
需要遍历,然后匹配,返回对应的位置;遍历过程参考尾插的过程
程序:

SLTNode* SLTSearch(SLTHead* phead,Datatype x)
{SLTNode* pcur = phead;while(pcur != NULL){if(pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}

第2.1个问题,打印的步骤是什么?
还是遍历,然后一个个打印出来

void SLTPrint(SLTHead* phead)
{SLTNode* pcur = phead;while(pcur != NULL){printf("%d->",pcur->data);pcur = pcur->next;}printf("NULL");
}

第2.2 个问题
传参为何不用传 SLTNode** ?
因为
在Print中我们只是范围对应的结构体,使用结构体的地址就可以(只是没必要用指针的地址,但是是可以用的)
在Search中我们也没有对结构体的地址进行修改,只是得到对应地址的值,因此也只需要传结构体的地址

在 某位置后插入删除

第1.1个问题
为什么要先说在 pos(位置的代称) 后插入呢?
单链表的特性就是,可以通过上一个节点的 next 寻找到下一个节点,只要我们知道上一个节点的地址,就很容易能够找到下一个节点
第1.2个问题
如何找到上一个节点的位置?
记得在上一个小节,我们写了一个SLTSearch
我们可以通过这个函数找到有对应数据的节点

第1.3 个问题
步骤是什么

  1. 找到储存对应数据的节点的地址
  2. 生成新的节点
  3. 将节点链接到原来的链表

图例解释
在这里插入图片描述

第1.4个问题
链接的顺序是什么?
从图上可以看出,是 newnode 先链接到后面,然后再修改上一个节点的指向
因为是单链表,如果先将 pos -> next 指向newnode
pos 原来的后段就丢失了,导致出错

第1.5个问题
如果没有找到对应的节点怎么办?
因为定义中 Search 若没有找到对应的节点,由于这里是在某位置后插入,所以找不到就无法再插入了,就退出了

void SLTInseartBack(SLTNode* pos,SLTDatatype x)
{if(pos == NULL){return ;}SLTNode* newnode = BuyNewNode(x);newnode->next = pos->next;pos->next = newnode;
}
//这里是调用
void Test1()
{PList* phead;//假设这是一个已知链表的头结点的地址SLTNode* pos = SLTSearch(phead,x);SLTInseartBack(pos,x);
}

这里相似的,就会有在某个位置后删除
步骤是:

  1. 找到某个位置
  2. 找到pos ->next->next
  3. 将pos->next 删除
    图解
    在这里插入图片描述
    第2.1 个问题,如果没找到或者pos 没有元素呢?
    在这两种情况下都不能删除
    对应程序
void SLTDelBack(SLTNode* pos)
{if(pos == NULL || pos->next == NULL ){return ;}SLTNode* pfree = pos->next;pos->next = pos->next->next;free(pfree);
}

在某位置的插入删除

难点: 链表指针无法访问到前一个元素
首先第1.1个问题
我们想用这个函数做什么?
我们可以通过 Search 找到对应的 pos
我们想直接在 pos 的位置上插入或者删除元素,这是我们的目的
第1.2 个问题
这个函数实现的难点是什么?
因为是单链表,不能找到pos 前一个位置,所以需要遍历找到前一个位置
具体步骤

  1. 利用 search 找到 pos
  2. 利用遍历找到pos 前一个元素的位置
  3. 在前一个位置的后面插入删除
    图例解析
    在这里插入图片描述

第1.3个问题
如果pos 是空是什么情况?
如果 pos 是空,那么就意味着是链表的尾部,这就说明是尾插和尾删

第1.4个问题
如果一开始就没有元素呢?
一开始没有元素就需要改变头结点指针的参数
所以传参需要传头结点指针的地址
相当于空链表的尾插

第1.5个问题
如果 pos指向的是第一个元素呢?
我们需要从让新元素排到第一个
这个时候情况就变成了头插

所以对应的程序是

void SLTInsert(SLTNode** pphead,SLTNode* pos,Datatype x)
{assert(pphead);SLTNode* pcur = *pphead;if(pcur == NULL || pos == NULL){SLTPushBack(pphead,x);return ;}if (pcur == pos){SLTPushFront(pphead,x);return;}SLTNode* newnode = BuyNewNode(x);SLTNode* newnode = BuyNewNode(x);SLTNode* pnext = pcur->next;while(pnext != pos){pcur = pnext;pnext = pnext->next;}newnode->next = pnext;pcur->next = newnode;}

相对应的也有对应位置的删除
什么步骤呢?

  1. 找到对应的位置
  2. 利用遍历找到pos 上一个节点
  3. 删除pos的元素

问题2.1
尾删是不是一种特殊情况呢?
这个时候尾删, pos 就必须指向一个确定的位置,和普通情况一样,因此不用特殊处理
即使是只剩下一个节点,也可以当做头删来处理

图例解析
在这里插入图片描述

void SLTErase(SLTNode** pphead,SLTNode* pos)
{assert(pphead);SLTNode* pcur = *pphead;if(pos == NULL){return ;}if(pos == pcur){SLTPopFront(pphead);return;}SLTNode* pre = pcur;while(pcur != pos){pre = pcur;pcur = pcur->next;}pre->next = pcur->next;free(pcur);}

注意:
有一个值得注意的地方
就是 每次 pos 在 位置删除后,这个pos 就不能用了,要在函数外置为 NULL

销毁链表

销毁链表的步骤是?
遍历然后一个个销毁
但是每次要把将要销毁的元素的后一个元素保存起来

图例解析
在这里插入图片描述
程序实现

void SLTDestroy(SLTNode* pphead)
{assert(pphead);SLTNode* pcur = *pphead;SLTNode* pre = *pphead;while(pcur != NULL){pre = pcur;pcur = pcur->next;free(pre);}*pphead = NULL;
}

希望大家看完,能够有所收获
如果有错误,请指出我一定虚心改正
动动小手点赞
鼓励我输出更加优质的内容

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

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

相关文章

显卡---显卡驱动---CUDA---Cudnn

1. 背景 最近在follow百度的CAE这篇论文时&#xff0c;源码需要的环境为&#xff1a; python 3.7 cuda: 11.0 cudnn: 8.0.4 gcc 8.2 该版本要求与我目前使用的服务器上的CUDA版本不相符合。因此搜索了一篇国外小哥的文章&#xff0c;讲述了如何在一台服务器上安装多个CUDA和Cud…

Node之Express学习笔记

一.Express 1.1什么是Express Express的作用和Node.js内置的http模块类似 专门用来创建Web服务器的 本质&#xff1a;npm上的第三方包&#xff0c;提供快速创建Web服务器的便捷方法 1.2Express能做什么 Web网站服务器&#xff1a;专门提供Web网页资源的服务器 API接口服务器&…

FPGA----ZCU106基于axi-hp通道的pl与ps数据交互(全网唯一最详)

1、大家好&#xff0c;今天给大家带来的内容是&#xff0c;基于AXI4协议的采用AXI-HP通道完成PL侧数据发送至PS侧&#xff08;PS侧数据发送至PL侧并没有实现&#xff0c;但是保留了PL读取PS测数据的接口&#xff09; 2、如果大家用到SoC这种高级功能&#xff0c;那大家应该对于…

Linux进阶-进程间通信(ipc)

进程间通信&#xff1a;数据传输、资源共享、事件通知、进程控制。 Linux系统下的ipc 早期unix系统 ipc&#xff1a;管道&#xff08;数据传输&#xff09;、信号&#xff08;事件通知&#xff09;、fifo&#xff08;数据传输&#xff09;。 system-v ipc&#xff08;贝尔实…

Kubernetes之Pod初始化容器

Kubernetes之Pod初始化容器 概述 ​ 初始化是很多编程语言普遍关注的问题&#xff0c;甚至有些编程语言直接支持模式构造来生成初始化程序&#xff0c;这些用于进行初始化的程序结构称为初始化器或初始化列表。初始化代码要首先运行&#xff0c;且只能运行一次&#xff0c;它们…

CAPM资产定价模型

本文是Quantitative Methods and Analysis: Pairs Trading此书的读书笔记。 CAPM(Capital Asset Pricing Model)资产定价模型 这个模型在金融界的影响就是beta这个词的使用。CAPM模型用两个部分的回报之和来解释一个资产的回报。其中一个部分是指市场&#xff08;称为market)…

时间序列:时间序列模型---自回归过程(AutoRegressive Process)

本文是Quantitative Methods and Analysis: Pairs Trading此书的读书笔记。 这次我们构造一个由无限的白噪声实现&#xff08;white noise realization) 组成的时间序列&#xff0c;即。这个由无限数目的项组成的值却是一个有限的值&#xff0c;比如时刻的值为&#xff0c; 而…

Magic Leap 2设计和开发幕后花絮

Magic Leap今年发布新款AR头显Magic Leap 2&#xff0c;相比于上一代Magic Leap 1&#xff0c;新品更专注于B端场景&#xff0c;自公布以来&#xff0c;Magic Leap不仅对公司策略、理念更加透明&#xff0c;也不断公开ML2产品设计背后的思考。相比于ML1&#xff0c;ML2的设计有…

粒子群算法查找函数最小值

实现 函数 F01、F06 的优化测试 以下内容是现有算法的运行结果、调参分析、及代码实现&#xff0c;用于给其他人提供参考&#xff0c;懒得改了hh 1. 运行结果 参数 w 0.5 &#xff08;可更改&#xff09; c1 2.0 &#xff08;可更改&#xff09; c2 2.0 &#xff08;可更改&…

Echart 柱状图,X轴斜着展示

option { color: [‘#3398DB’], tooltip: { trigger: ‘axis’, axisPointer: { // 坐标轴指示器&#xff0c;坐标轴触发有效 type: ‘shadow’ // 默认为直线&#xff0c;可选为&#xff1a;‘line’ | ‘shadow’ } }, grid: { left: ‘3%’, right: ‘4%’, bottom: ‘3%’…

iPhone升级iOS 16后出现提示“面容ID不可用”怎么办?

最近&#xff0c;很多用户在苹果社区反馈&#xff0c;iPhone升级iOS 16后Face ID不能用了&#xff0c;尝试重置Face ID时&#xff0c;系统会弹窗提示“面容ID不可用&#xff0c;稍后尝试设置面容ID。” 如果你的iPhone在没有摔落手机或是手机进水的情况下出现这个弹窗&#xff…

【uniapp小程序】路由跳转navigator传参封装

文章目录&#x1f34d;前言&#x1f34b;正文1、看官网1.1 navigator API 介绍1.2、路由跳转参数传递1.3、五种常见的跳转方式1.3.1 uni.navigateTo(OBJECT)1.3.2 uni.redirectTo(OBJECT)1.3.3 uni.reLaunch(OBJECT)1.3.4 uni.switchTab(OBJECT)1.3.5 uni.navigateBack(OBJECT)…

图的初识·基本概念

文章目录基本概念图有两种基本形式无向图的表示有向图的表示基本概念 图结构也是数据结构的一部分。而且还有一点小难。图是由多个结点链接而成的&#xff0c;但是一个结点可以同时连接多个其他结点&#xff0c;多个节点也可以同时指向一个节点。【多对多的关系】 图结构是任意…

如何从报表控件FastReport .NET中连接到 PostgreSQL 数据库?

FastReport.NET官方版下载 Fastreport是目前世界上主流的图表控件&#xff0c;具有超高性价比&#xff0c;以更具成本优势的价格&#xff0c;便能提供功能齐全的报表解决方案&#xff0c;连续三年蝉联全球文档创建组件和库的“ Top 50 Publishers”奖。 慧都科技是Fast Repor…

mysql索引类别和失效场景

首先&#xff0c;我们为什么要使用索引&#xff0c;索引有什么作用呢&#xff1f; 索引可以用来快速查询数据表中有某一特定值的记录&#xff0c;大大加快数据的查询速度&#xff1b;在列上创建了索引之后&#xff0c;查找数据时可以直接根据该列上的索引找到对应记录行的位置…

YOLO X 改进详解

YOLO X 主要改进&#xff1a; Anchor-Free: FCOSDecoupled detection headAdvanced label assigning strategy Network structure improvement Decoupled detection head 对比于YOLO V5, YOLO X 在detection head上有了改进。YOLO V5中&#xff0c;检测头是通过卷积同时预…

视频编解码 - RTP 与 RTCP

目录 RTP 实时传输协议 RTCP协议 将H264 RTP打包 RTP 实时传输协议 音视频数据传输&#xff0c;先将原始数据经过编码压缩后&#xff0c;将码流打包成一个个RTP包&#xff0c;再将码流传输到接收端。 打包的作用 接收端要正确地使用这些音视频编码数据&#xff0c;不仅仅需…

JaVers:自动化数据审计

在开发应用程序时&#xff0c;我们经常需要存储有关数据如何随时间变化的信息。此信息可用于更轻松地调试应用程序并满足设计要求。在本文中&#xff0c;我们将讨论 JaVers 工具&#xff0c;该工具允许您通过记录数据库实体状态的更改来自动执行此过程。 Javers如何工作&#x…

vue Router

Vue项目各文件含义 1.src文件夹 是我们真正敲代码的文件夹&#xff0c; 2.assets放静态资源 3.components放组件 4.App.vue主组件 5.main.js项目的入口文件 Vue Router 在router/index.js路由文件中配置路由&#xff0c;设置路由跳转规则 import Vue from vue import Vu…

android Framework 中用到了哪些跨进程通信方式?

文章目录Linux 有哪些跨进程的通信方式&#xff1f;管道本地 Socket共享内存信号Linux 有哪些跨进程的通信方式&#xff1f; Binder 机制是Android基于Linux的一种独特的IPC机制。我们常用的AMS&#xff0c;PMS 等都是通过Binder机制来完成跨进程通信的&#xff0c;那么除了Bin…