手把手教你写链表,小学生看了都说好~

2023-05-16

摘要:明明我们在之前已经接触了数组,感到数组已经是万能的数据存储位置了。但是,如果我们一直在使用比较复杂的数据(也就是比较多的数据时),肯定会感到很反感。因为对于数组这种数据结构,在你自己使用之前,一定要对其大小进行一番定义。这样一来,它的存储空间在数据处理过程中便显得极为不方便。因为谁也不想对将要处理的数据做一个空间的预算,这几乎是所有程序员都很忌讳的,并且还要让其空间足够大,这样才能满足我们的要求(但如果分配的太多,难免会浪费内存)。

 

所以,这就是为啥你要用链表、学链表

链表是一种数据结构,它弥补了数组带来的诸多不便,让我们可以任意为一些数据进行空间的分配,根据需要进行内存单元的开辟。

提到链表就必须要提到一个比较重要的知识点就是指针,因为前后数据要有关联,必须要进行一系列的连接和指向处理,那么扮演这个角色的就是指针,并且现在的编程语言中,指针是任何东西都没有办法去替代的

如果你不太理解指针,那么建议你看看这篇文章:手把手教你写单片机的指针。

当然学习链表前,你还要有结构体的基本概念和知识,为啥?因为链表就是通过指针连接的多个结构体。每一个结构体中有一个存放指针的成员变量,并且,这个成员的类型是该结构体类型。每一个链表,都有这个自己的结点,这些结点是结构体的变量,当然,他们也是结构体类型的变量。

如果你不太理解结构体,那么建议你看看这篇文章:手把手教你写单片机的结构体。

当你看完理解了指针和结构体,如果想再进一步学习一下链表,然后再看这篇文章,你一定会有一种恍然大悟的感觉,你会发现指针、结构体、链表其实也不是很难。

话不多说,下面开始!

一、链表

1、链表基础知识

我们假设有三个特务,ABC。A的下线是B,B的下线是C。换句话说,就是A有B的地址,B有C的地址。写成代码就是这样:

//注::如果这个结构体你看不懂,回去补结构体的知识
typedef struct spy 
{
 char *name;
 struct spy *next;
}spy, *p_spy;

spy A = {"A", NULL};
spy B = {"B", NULL};
spy C = {"C", NULL};
spy D = {"D", NULL};

int main( void )
{
 p_spy pHead = NULL;
 
 A.next = &B;  //A的下线是B
 B.next = &C;  //B的下线是C
 C.next = NULL;//C没有下线
 
 pHead = &A; 
}

画一个图:

 首先,我们定义了三个结构体,ABC。在内存里面就必定有这三个结构体。再看看下面这句话,它会导致什么结果:A.next_addr=&B

记住上面这个图,结构体A里面的next_address,等于B的地址。那蓝色箭头是我画出来的,只是给我们形象的认识而已,结构体A里面保存有B的地址。链表的核心就是:

这个链表结构体里面有一个指针,这个指针,等于其他结构体的地址

用人类形象化的话来说,就是结构体A里面的某一个指针,指向结构体B

首先作为一个链表,肯定有头部呀,我们怎么来确定这个链表的头部?实际上我们用一个指针来表示链表头,代码如下:

 

typedef struct spy {
 char *name;
 struct spy *next;
}spy, *p_spy;
p_spy pHead = NULL;

看一下这段代码,我们定义了三个结构体,还有一个结构体指针。它们都是变量,在内存里面都有对应的空间。

在上面的图里面,在红色方框里,我们用那个指针来表示链表头,现在这个链表头,它的值它是空的,也就是说它里面保存的地址是空的:这是一个空链表。

那么我们怎么往这个链表里面添加一个元素呢?先用一个图来表示,假设把结构体A放到列表里面去:

 再看一下插入第一项非常简单,我让这个链表头直接等于结构体A的地址就可以了。我们用箭头来表示,让我们更加形象的去了解这个链表:

 在这个图里面加了这个箭头,在代码里面可没有什么箭头,它只是pHead这个变量,它的值等于结构体A的地址。

2、链表的插入

现在再把一个结构体B放入链表。有两种方法,你是放在链表头部?还是放在链表尾部?画出一个图:

链表里面只有元素A,我们可以在A的左边插入这个新的元素B,也可以在A的右边插入这个新的元素B。也就是说,我们可以在链表的头部插入新的节点,也可以在列表的尾部插入新的节点。在图里面,上面那个就是把B插在链表的尾部,下面那个就是把B插在链表的头部。怎么写代码呢?

我们先来看看,把B插在链表的头部:

void InsertNodeToHead(spy newNode)
{
 newNode->next_addr = pHead;//新插入节点的下一个节点为空
 pHead = newNode; //头结点指向新插入的节点
}

画个图演示一下:

在这个图里面,左边是代码,右边是结果。假设一开始的时候先插入结构体A,执行图中标号为2的代码的时候,就是:A.next_addr=pHead等于初始值NULL。执行上图中标号为3的代码的时候,就是:pHead=A的地址。结果在上图里面右边地方,在图里面也写出了标号2、标号3。标号2那里A的next.address等于NULL,标号3那里pHead等于结构体A的地址。

下面,我们再来增加第2个元素B,我们在链表的头部插入元素B。

 

在这个图里面用蓝色的标号,把调用过程给标了出来。在左边是代码,看标号为4的代码,要用这个函数把元素B插入链表。怎么做呢?也分为两步:

  • 第1步

不是插到头部去吗?那我就让B指向头部:

但头部等于A,也实际上就是B指向了A。

也就说现在B指向了A,头部也指向了A。

  • 第二步:

让头部要指向B:

 

 这就是一个完整的插入过程。这个图还要补充一下,让结尾指向NULL。

 

把链表,想成一个手拉着手的队伍,就容易理解了。刚才我们讲的是在链表的头部插入一个元素,那怎么在一个链表的尾部插入一个元素呢?

我们假设这个图里面它有好几个元素,我们在最后面一个元素的右边,再插入新元素。

 得到的结果如下图所示:

 

tmp假设是最后一个元素,B是新元素。

tmp->next.addr=&B;
B.next addr = NULL;

问题的关键在于,我怎么在原来的列表里面找出最后一个元素。

主要是需要定义一个临时节点,然后使用一个while循环:

void insertNodeToTail(p_spy newNode)
{
 p_spy  temp;//定义一个临时节点
 temp = head;//让头结点指向临时节点
 while(temp)//在原来的列表里面找出最后一个元素
 {
  if (temp ->next == NULL)//找到了最后一个节点 temp就是最后一个节点
   break;
  else
   temp = temp ->next;
 }
 temp ->next = newspy;//最后一个节点插入新节点
 newspy->next = NULL;//新节点的下一个节点为空,这样就将新节点插入最后了
}

图中红圈处,它的特征是什么:它的next.addr等于NULL。如果不是最后一项的话,我们就取出他右边的那一项:temp = temp -> next.addr。这句话可能有些同学理解起来困难,这里画个图解释一下:

 插入尾部的代码:

 

void InsertNodeToTail(p_spy newNode)
{
 p_spy  temp;//定义一个临时节点
 if(pHead == NULL)//如果链表为空
 {
  pHead = newNode;
  newNode->next = NULL;
 }
 else
 {
  temp= pHead ;//让头结点指向临时节点
  while (temp)
  {
   if (temp->next == NULL) //找到了最后一个节点 temp就是最后一个节点
    break;
   else
    temp= temp->next;
  }
  temp->next = newNode;//最后一个节点插入新节点
  newNode->next = NULL;//新节点的下一个节点为空,这样就将新节点插入最后了
 }
}

3、链表的删除

在链表中,怎么删除一个元素?

 再看这个图,在链表中我们要删除红色方框的这个节点。再想象一下,在一个手牵着手的队伍里面,有一个人要走了,是不是他前面那个人要跟后面那个人牵手?

 所以我们要找出前面那个人和后面那个人。假设temp是前面那个人,后面那个人是谁?needDeleteNode->nextneedDeleteNode表示要删除的节点。代码怎么写呢:temp->next = 后面的人 = needDeleteNode->next所以关键在于我们怎么找到前面的人:temp。这也比较简单,遍历链表:

void RemoveNode(p_spy needDeleteNode)
{
 p_spy temp;//定义一个临时的节点,用来遍历
 if (pHead == needDeleteNode)//如果被删除的节点正好是头结点指向的那一个节点
 {
  pHead = needDeleteNode->next;//直接让头节点指向被删除节点的下一个节点
  return;//返回
 }
 else
 {
  /* 找出NeedDeleteNode的上一个节点 */
  temp = pHead;//让头结点指向临时节点temp
  while(temp)
  {
   if (temp->next == needDeleteNode)//找到了,要删除的节点就是temp的下一个节点
    break;
   else
    temp= temp->next;//继续找
  }  
  if (temp)
  {
   temp->next = needDeleteNode-> next;//让被删除的上一个节点temp的下一个节点指向被删除节点的下一个节点,那么就把原来的要删除的节点给从链表中删除了。
  }
 }
}

也是一个循环,如果我的下一项就等于你的话,我就是你的前一个。找到之后,就执行这条指令:temp->next=后面的人=needDeleteNode->next

4、链表的使用

/*------------------------1.链表和结点的定义----------------------------*/

/*结点结构体*/
typedef struct LIST_NODE {
    int data;                        /*用于存放结点数据*/
    struct LIST_NODE *pxNext;        /*用于指向下一个结点*/
    struct LIST_NODE *pxPrevious;    /*用于指向上一个结点*/
}ListNode;


/*链表结构体*/
typedef struct LIST {
    unsigned int NumberOfNodes;      /*用于记录链表结点数量*/
    ListNode RootNode;               /*用于作为循环链表的参考点*/
}List;

/*------------------------2.链表和结点的初始化---------------------------*/
/*结点初始化*/
void ListInitialiseItem(ListNode *pxListNode, int value)
{  
 pxListNode->data = value;       /*结点数据赋值*/
}

/*链表初始化*/
void ListInitialise(List *pxList)
{
    pxList->RootNode.pxNext = &(pxList->RootNode);     /*由于此时链表中没有结点,第一个结点指向自己*/
    pxList->RootNode.pxPrevious = &(pxList->RootNode); /*由于此时链表中没有结点,第一个结点指向自己*/

    pxList->NumberOfNodes = 1;                         /*链表结点计数初始化为1,也就是只有一个根结点*/     
}

/*------------------------3.1结点插入链表---------------------------*/
void ListInsertEnd(List *pxList, ListNode *pxInsertListNode)
{
    ListNode *pxNextNode = &(pxList->RootNode);                /*插入结点的后结点*/
    ListNode *pxPreviosNode = pxList->RootNode.pxPrevious;     /*插入结点的前结点*/


    pxInsertListNode->pxNext = pxNextNode;                     /*插入结点指向后结点*/
    pxInsertListNode->pxPrevious = pxPreviosNode;              /*插入结点指向前结点*/ 

    pxPreviosNode->pxNext = pxInsertListNode;                  /*前结点指向插入结点*/   
    pxNextNode->pxPrevious = pxInsertListNode;                 /*后结点指向插入结点*/   

    (pxList->NumberOfNodes)++;                                  /*链表结点计数加1*/
}

/*------------------------3.2链表删除结点---------------------------*/
void ListRemove(List *pxList, ListNode *pxIListToRemove)
{
  ListNode *pxPreviosNode = pxIListToRemove->pxPrevious;     /*删除结点的前结点*/
  ListNode *pxNextNode = pxIListToRemove->pxNext;            /*删除结点的后结点*/

  pxNextNode->pxPrevious = pxPreviosNode;                    /*后结点指向前结点*/
  pxPreviosNode->pxNext = pxNextNode;                        /*前结点指向后结点*/

  (pxList->NumberOfNodes)--;                                 /*链表结点计数减1*/
}


int main(void)
{
 /*1.定义链表、结点*/
 List      list;         //定义链表
 ListNode  list_node1;   //定义结点1
 ListNode  list_node2;   //定义结点2

 /*2.初始化链表、结点*/
 ListInitialise(&list);
 
 ListInitialiseItem(&list_node1, 100);
 ListInitialiseItem(&list_node2, 200);
 
 /*3.插入链表*/
 ListInsertEnd(&list, &list_node1);
 ListInsertEnd(&list, &list_node2);
 
 /*4.删除结点*/
 ListRemove(&list, &list_node1);
 
 return 0;
}

首先,我们定义了一个结构体:List list。它是一个变量,在内存里面必定有对应的空间。初始化完这个链表之后,它的结果就像上面的图表示的那样。因为这个链表内部有一个根节点,所以把它的节点个数设置为1。

这个链表一开始的时候只有一个元素,它的下一个元素是它自己,它的上一个元素也是它自己。这是一个双向的循环链表,双向循环链表稍微复杂一点,但是再怎么复杂,也就是使用两个单向链表组成的。

 怎么样?链表是不是也没那么难?

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

手把手教你写链表,小学生看了都说好~ 的相关文章

  • Sonic测试架构介绍

    Sonic项目简介 Sonic Software for Open Networking in the CloudSonic是基于Linux的开源网络操作系统 xff0c 可以跑在多个不同芯片厂商交换机上Sonic在2016年OCP峰会上
  • Sonic_cli常用命令

    用户名 xff1a admin 密码 xff1a YourPaSsWoRd 一 change password admin 64 sonic passwd Changing password for admin current UNIX p
  • SONIC config_db.json文件的前生今世

    config db json的使用 系统启动时从config db json中读取数据并写入CONFIG DB数据库 xff0c 前提是config db json存在 xff1b 保存当前系统的一些配置信息 xff0c 通过config
  • SONiC架构DOCKER组件交互分析

    BGP组件交互分析 内核中的bgp socket收到BGP更新报文 xff0c 然后被上送到bgpd进程bgpd处理该报文 xff0c 并通知zebra进程新增前缀和关联下一跳zebra确定该目的可达后 xff0c 生成一个路由网络链接信息
  • sonic处理netlink事件

    sonic处理netlink事件 sonic在处理路由 xff0c 接口up down 接口地址变化 xff0c team等事件上极大的依赖内核 sonic通过监听rtnl事件来响应linux事件 从而感知相关信息变化 libnl soni
  • sonic配置team与实现机制

    sonic实现team代码框架图 xff1a sonic修改lag模式配置步骤 1 修改文件teamd j2 docker exec it teamd bash cd usr share sonic templates vim teamd
  • asyncComputed 异步计算属性

    asyncComputed 异步计算属性 我们项目中最近使用了异步计算属性 xff0c 个人感觉很好用 xff0c 特此推荐给大家 一 案例 假设这样一个场景 xff1a 一个列表数据 data 是通过接口返回 xff0c 请求参数中有一个
  • sonic管理口信息处理流程

    sonic管理口信息处理流程 管理接口信息配置文件格式 管理信息使用MGMT INTERFACE 表进行配置 对象的key由管理接口名字和IP前缀使用 连接而成 属性 gwaddr用于执行默认路由指向管理口 xff0c 其值为默认网关 属性
  • SONIC VLAN配置流程

    SONIC VLAN配置流程 sonic vlan配置通过订阅config db的键空间事件完成vlan配置信息从config db到内核和硬件 config db json格式如下 xff1a 34 VLAN 34 34 Vlan1000
  • sonic容器构建

    sonic中大量的组件运行在docker容器中 xff0c 用于隔离彼此的运行环境 xff0c 从而解决相互之间的互斥问题 下面我们分析一下sonic中各个容器的构建过程 Dockerfile文件的生成过程 sonic中的容器Dockerf
  • ARM Cortex-M底层技术(九)KEIL MDK 分散加载示例1-更改程序运行基址

    KEIL MDK 分散加载示例1 更改程序运行基址 小编我一向主张在实战中学习 xff0c 不主张直接去去学习规则 amp 定义 xff0c 太枯燥 xff0c 在实际应用中去摸索 xff0c 才会真正理解具体的技术细节 xff0c 下面我
  • 如何修改已提交commit信息

    如何修改已提交commit信息 1 修改commit信息 1 1 修改最后一次提交信息 通过git log查看提交历史信息 xff1a 输入命令 xff1a span class token function git span commit
  • linux 安装mysql 以及常见错误总结

    安装的方法菜鸟教程上有 xff1a 入口 mysqld initialize 命令是出现如下错误Fatal error Please read 34 Security 34 section of the manual to find out
  • elementui下拉框多选报[Vue warn]: <transition-group> children must be keyed: <ElTag>

    elementui下拉框多选 xff0c 选值报错 选中一个值后所有的值都会被选中 经检查 xff0c 是由于我的下拉框的value值为一个对象而非单个值 值为对象时需要填入value key span class token operat
  • Hadoop_1 入门WordCount

    记录踩得坑以及部署环境流程 搭建的是伪分布Hadoop 首先环境需要安装zookeeper 这个好装 xff0c 不多说其次比较复杂的是安装openssh 我的Linux系统是centos 7 mini版本 安装openssh之前的准备工作
  • PX4无人机控制

    PX4无人机控制 向话题 mavros setpoint raw local发布无人机目标位置 43 偏航 xff0c 或者目标速度 43 偏航 发布目标位置 43 偏航 xff1a mission span class token pun
  • npm install 报错:gyp verb `which` failed Error: not found: python

    今天新启动一个项目 xff0c 在 npm install 安装依赖项时出现报错 npm install span class token operator gt span husky 64 span class token number
  • 24_ZYNQ7020开发板_SPI与Qflash芯片读写

    内核中使用spi master表示一个SPI主机控制器 一 SPI控制器驱动 1 xff09 spi master申请与释放 SPI申请 span class token keyword struct span spi master spa
  • python3的input函数实现回车换行,而不是结束输入

    span class token comment 实现回车换行 xff0c 而不是结束 span endstr span class token operator 61 span span class token string 34 34
  • rviz-Ros Wiki官网教程学习笔记(1)-用户指南

    0 rviz介绍 rviz是ROS自带的图形化工具 xff0c 可以很方便的让用户通过图形界面开发调试ROS 参考官网 rviz UserGuide 1 安装 根据自己的版本号 xff0c 在下面选择对应的命令执行 以ROS melodic

随机推荐

  • 轻松搭建CAS 5.x系列(6)-在CAS Server上增加OAuth2.0协议

    概述说明 CAS Server默认搭建出来 xff0c 客户端程序只能按照CAS自身的协议接入 CAS的强大在于 xff0c 有官方的插件 xff0c 可以支持其他的协议 本章节就让CAS Server怎么增加OAuth2 0的登录协议 安
  • 一起DIY四轴飞行器(二)初识飞控大脑

    系统 xff1a Windows 硬件 xff1a xff08 1 xff09 STM32F103C8T6最小系统板 某宝上搜索 STM32F103最小系统 xff0c 如上图所示这样的 xff08 2 xff09 ST LINK v2下载
  • STM32CubeMX基于HAL库点亮LED灯

    开发环境 xff1a Windows 软件 xff1a 1 STM32CubeMX 6 3 0 2 MDK 5 14 一 cubeMX的基本配置 1 选择MCU 2 配置时钟 查看电路图 xff0c 8MHz的高速外部晶振接到 OSCIN
  • 一起DIY四轴飞行器(三)添加实时操作系统--freeRTOS

    开发环境 xff1a Windows 软件 xff1a 1 STM32CubeMX 6 3 0 2 MDK 5 14 一 初识freeRTOS系统 1 什么是FreeRTOS xff1f Free 即免费的 xff0c RTOS 全称是 R
  • ESP8266 初次使用

    一 工具 1 ESP8266 01S 淘宝上搜索ESP8266 01S 引脚说明 xff1a 2 USB转串口 给设备供电3 3V xff08 官方说不要用USB转串口的3 3V xff0c 需要单独供电 xff09 xff0c USB转串
  • ubuntu安装boost

    ubuntu安装boost 系统 Ubuntu 18 04 boost中 xff0c 用到了别的函数库 xff0c 所以为了使用boost中相应的功能 xff0c 需要先安装系统中可能缺失的库 1 卸载已经安装的boost 删除 usr l
  • 简单的状态机图

    一 什么是状态机 xff1f 做产品的时候 xff0c 我们总能遇到一些比较复杂的逻辑问题 比如状态的转换 xff0c 字段状态的确认 xff0c 权限的控制 xff0c 状态的对应 而普通的流程图 xff0c 或时序图 xff0c 更侧重
  • js-对象转基本类型

    起因是收到朋友发的一道题 xff0c 如下 xff1a span class token comment 请在问号处填写你的答案 使下方等式成立 span span class token keyword let span a span c
  • 局域网内wakeonlan远程唤醒其它计算机

    背景 xff1a 需要管理多台计算机 xff0c 所有计算机在一个局域网内 xff0c 并且有的安装了Windows系统 xff0c 有的安装了Linux系统 我们想远程关闭和启动所有计算机 关闭计算机直接通过网络发生操作系统关机命令即可实
  • D3D中的三种Buffer

    在D3D中 针对视窗有三种Buffer 它们分别是 Color Buffer Depth Buffer和Stencil Buffer Color Buffer在D3D中又称为Render Target 意思是最后着色的目标Buffer 就是
  • 创建镜像(更新与构建镜像)

    创建镜像 有时从Docker镜像仓库中下载的镜像不能满足我们的要求 xff0c 此时可以基于这个镜像 xff08 基础镜像 xff09 封装一个自 己的镜像 两种方式 xff1a 更新镜像 xff1a 使用docker commit命令构建
  • (十)CMake链接已有的动态库

    使用一个已经存在的动态库 xff0c 需要用到CMake中两个命令 xff0c 分别是 xff1a link directoriestarget link libraries 下面先介绍以下两个命令的格式及其含义 xff0c 最后是一个使用
  • ROS入门21讲笔记(四)自定义话题消息类型和使用

    除了ROS内置消息外 xff0c 我们还能自定义消息 这一次我们不再与海龟较劲 xff0c 而是自定义一个订阅消息类型 xff0c 让订阅者和发布者通过这个结构进行数据通信 一 如何自定义话题消息 xff1f 话题消息是以 msg结尾的文件
  • ROS入门21讲笔记(七)自定义消息消息类型和使用

    这一节主要是学习如何自定义一个服务类型并使用它 一 如何自定义服务消息 xff1f 服务数据是以 srv结尾的文件 xff0c 是一个用于描述ROS服务信息简单文本文件 xff0c 用于生成不同语言消息的源代码 srv文件存放在packag
  • ROS入门21讲笔记(十二)常用可视化工具

    一 QT类可视化工具 1 1 rqt console rqt console 为显示和过滤ROS信息提供了一个GUI插件 1 2 rqt plot rqt plot使用不同的绘图后端提供数值可视化功能 1 3 rqt Image view
  • (二)CMake 使用头文件

    一 include directories 该命令用于增加一个编译头文件 其基本语法是 xff1a include directories span class token punctuation span span class token
  • cargo 宏展开遇到的问题

    最近学习rust xff0c 看到宏展开命令 span class token comment 单独文件 span rustc Z unstable options pretty span class token operator 61 s
  • 工程师笔记|常见的嵌入式软件工程师面试题

    Q xff1a 什么是ISR xff1f A xff1a ISR 是指中断服务程序 这些是存储在特定内存地址的函数 xff0c 当发生某种类型的中断时会调用这些函数 Cortex M 处理器系列具有管理中断执行的 NVIC Q xff1a
  • 计算机中的速率、带宽、时延、利用率解读

    计算机网络的性能一般是指它的几个重要的性能指标 但除了这些重要的性能指标外 xff0c 还有一些非性能特征 xff08 nonperformance characteristics xff09 也对计算机网络的性能有很大的影响 那么 xff
  • 手把手教你写链表,小学生看了都说好~

    摘要 xff1a 明明我们在之前已经接触了数组 xff0c 感到数组已经是万能的数据存储位置了 但是 xff0c 如果我们一直在使用比较复杂的数据 xff08 也就是比较多的数据时 xff09 xff0c 肯定会感到很反感 因为对于数组这种