手撕数据结构 —— 单链表(C语言讲解)

目录

1.为什么要有链表

2.什么是链表

3.链表的分类

4.无头单向非循环链表的实现

SList.h中接口总览

具体实现

链表节点的定义

打印链表

申请结点

尾插

头插

尾删

头删

查找

在pos位置之前插入

在pos位置之后插入

删除pos位置

删除pos位置之后的值

5.完整代码附录


1.为什么要有链表

如果你学习过顺序表的话,应该清楚顺序表的缺点,如果你并不了解顺序表的话,推荐阅读一下这篇文章——手撕数据结构——顺序表(C语言讲解)

我们先来简单回顾一下顺序表,顺序表是在连续的内存空间上按顺序存储数据元素。

顺序表的优点:

  • 顺序表支持下标的随机访问和修改,并且时间复杂度是O(1),效率高。
  • 顺序表在尾部进行插入和删除效率高,时间复杂度是O(1)。

顺序表的缺点:

  • 头部和中部进行插入和删除效率都不高,时间复杂度是O(N)。
  • 动态顺序表扩容有一定消耗,尤其是动态扩容(需要开辟新空间,拷贝数据,释放旧空间)
  • 扩容造成的空间浪费(一般扩容是2倍扩容,假如容量从1000扩容到2000,但是我们只需要插入5个数据,就会浪费995个空间)

针对顺序表的缺点,有人设计出了另一种数据结构 —— 链表。

2.什么是链表

链表是一种利用不连续的内存空间存储数据元素的数据结构,并且元素之间可以不按顺序进行存储。

正是因为链表不按顺序存储,要想找到下一个数据就需要记录下一个数据的地址,因此,链表中的数据是包含在一个结构体中,我们通常称这个结构体变量为结点。最后一个结点没有下一个结点,就指向空。

链表的特点是按需申请和释放。

链表逻辑示意图如下:

需要注意的是:

  • 我们申请结点的时候,一般是使用动态内存管理的函数申请内存空间(malloc、realloc),这些函数是从堆上申请空间的。
  • 从堆上申请的空间,是按照一定的策略来分配的,这取决于操作系统,因此,两次申请的空间可能连续,也可能不连续。
  • 链表在逻辑结构上是连续的,在物理上结构上不一定连续,这取决于系统动态分配内存的位置。

3.链表的分类

链表大致可以分为这几类:单向还是双向、带不带头、循环还是非循环。

单向 or 双向:

带头 or 不带头:

循环 or 非循环:

通过数学知识,我们可以知道 2*2*2 = 8,也就是说,不同的分类可以组合出八种链表。 在众多的链表中,我们重点学习无头单向非循环链表带头双向循环链表。

无头单向非循环链表:也叫单链表,该链表结构简单,一般不会单独用来存储数据,而是用来作为其他数据结构的子结构。例如:哈希桶、图的邻接表……

带头双向循环链表:该链表是结构最复杂的链表,一般用来单独存储数据,实际中使用的链表,都是带头双向循环链表。

4.单链表的实现

说明一下:

实现链表的时候,主要实现无头单向非循环链表(单链表)带头双向循环链表,本篇文章中只实现无头单向非循环链表(单链表);带头双向循环链表的实现在下一篇文章中呈现。

实现链表我们定义两个文件,分别是SList.hSList.c,SList.h文件用来存放声明,SList.c文件用来存放定义。

SList.h中接口总览

具体实现

链表节点的定义

链表是由一个个动态申请的结点构成的,因此,我们要实现链表的第一件事就是先定义结点。链表的结点有两个成员,分别是数据域和指针域。

  • 数据域用来存放数据。
  • 指针域用来存放下一个结点的地址。

代码如下所示: 

打印链表

打印链表只需要知道链表的起始结点即可,创建一个临时变量作为phead的替身,每次打印完当前结点之后,cur就往后走。直到cur为空,表示没有元素了。

  • 注意:指针是可以作为逻辑条件判断的。

申请结点

结点的申请需要使用malloc函数,注意申请的类型是结点的类型,也就是结构体类型。

  • 结点申请成功之后,我们将数据域赋值为x,将指针域赋值为NULL。

尾插

尾插数据时,直接链接上去即可,但是,我们要考虑链表是否为空:

  • 如果链表为空,我们需要改变的是结构体的指针,需要传递二级指针。
  • 如果链表不为空,我们需要增加结点,使用结构体指针即可。(不同的结构体指针可以指向同一个结点,并操控同一个结点)

头插

进行头插操作,我们需要改变的也是结构体的指针,所以也需要传递二级指针。

  • 头插对于链表是否为空的情况处理是一样的。

尾删

进行尾部删除时需要考虑三种情况:

  • 链表为空:如果链表为空,不能删除。
  • 链表只有一个结点:如果链表只有一个结点,我们需要改变结构体指针,这也是为什么传递二级指针的原因。
  • 链表有一个以上的结点:此时,我们进行尾删时,只需要改变结构体,使用一级指针即可。

 

头删

头删需要判断链表是否为空,改变的也是结构体指针,所以也需要传递二级指针。

  • 头删只需要释放第一个节点,并改变pphead的值即可。

查找

遍历查找即可,找到返回对应结点的指针,没找到返回NULL。

  • 查找并不改变结构体指针,传递一级指针即可。

在pos位置之前插入

在pos位置之前插入需要记录pos的上一个位置。

该接口需要考虑三种情况:

  • 首先,链表不能为空,因为我们要在指定位置之前插入,如果链表为空,就不能指定位置了。
  • 在第一个结点前插入,这不就是头插吗?复用头插即可。
  • 在其他地方插入,链接顺序需要从后往前进行,并且还需要提前保存pos位置的上一个位置。

在pos位置之后插入

在pos位置之后插入,不需要记录上一个位置的情况,并且,也不需要判断pos的位置情况,因为,不管pos在哪里,都是一样的操作。

删除pos位置

删除pos位置和在pos位置之前插入一样,需要记录pos位置的上一个位置,需要分三种情况讨论:

  • 删除的时候,链表不能为空,删除位置不能为空。
  • 删除位置如果是第一个位置,直接复用头删即可。
  • 删除其他位置,需要记录上一个位置。

删除pos位置之后的值

删除pos位置之后的值不需要记录pos的上一个位置,但是需要注意不能删除尾结点,因为尾结点没有下一个结点。

5.完整代码附录

SList.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int SLTDataType;typedef struct SListNode                                     //定义结点 
{SLTDataType data;struct SListNode* next;
}SLTNode;void SLTPrint(SLTNode* phead);                               //打印链表 SLTNode* BuySListNode(SLTDataType x);                        //申请结点 void SLTPushBack(SLTNode** pphead, SLTDataType x);           //尾插 void SLTPushFront(SLTNode** pphead, SLTDataType x);          //头插 void SLTPopBack(SLTNode** pphead);                           //尾删 void SLTPopFront(SLTNode** pphead);                          //头删 SLTNode* SLTFind(SLTNode* phead, SLTDataType x);             //查找 void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x); //在pos之前插入xvoid SLTInsertAfter(SLTNode* pos, SLTDataType x);              //在pos之后插入xvoid SLTErase(SLTNode** pphead, SLTNode* pos);                 //删除pos位置的元素 void SLTEraseAfter(SLTNode* pos);                              //删除pos的后一个位置

SList.c

#include"SList.h"// 打印链表 
void SLTPrint(SLTNode* phead)
{SLTNode* cur = phead;//while (cur != NULL)while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}// 申请结点 
SLTNode* BuySListNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}// 尾插 
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuySListNode(x);if (*pphead == NULL){// 改变的结构体的指针,所以要用二级指针*pphead = newnode;}else{SLTNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}// 改变的结构体,用结构体的指针即可tail->next = newnode;}	
}// 头插 
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuySListNode(x);newnode->next = *pphead;*pphead = newnode;
}// 尾删 
void SLTPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead);if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* tail = *pphead;while (tail->next->next){tail = tail->next;}free(tail->next);tail->next = NULL;}
}// 头删 
void SLTPopFront(SLTNode** pphead)
{assert(pphead);assert(*pphead);SLTNode* newhead = (*pphead)->next;free(*pphead);*pphead = newhead;
}// 查找元素 
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}//在pos位置之前插入x 
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);if (pos == *pphead){SLTPushFront(pphead, x);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}SLTNode* newnode = BuySListNode(x);prev->next = newnode;newnode->next = pos;}
}// 在pos之后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySListNode(x);pos->next = newnode;newnode->next = pos->next;
}// 删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(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的后一个位置
void SLTEraseAfter(SLTNode* pos)
{assert(pos);// 检查pos是否是尾节点assert(pos->next);SLTNode* posNext = pos->next;pos->next = posNext->next;free(posNext);posNext = NULL;
}

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

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

相关文章

理解Web3的互操作性:不同区块链的连接

随着Web3的迅速发展&#xff0c;互操作性成为区块链技术中的一个核心概念。互操作性指的是不同区块链之间能够无缝地交流和共享数据&#xff0c;从而实现更加高效和灵活的生态系统。本文将探讨Web3中互操作性的意义、面临的挑战以及未来的发展趋势。 1. 互操作性的意义 在Web…

如何用深度神经网络预测潜在消费者

1. 模型架构 本项目采用的是DeepFM模型&#xff0c;其结构结合了FM&#xff08;因子分解机&#xff09;与深度神经网络&#xff08;DNN&#xff09;&#xff0c;实现了低阶与高阶特征交互的有效建模。模型分为以下几层&#xff1a; 1.1 FM部分&#xff08;因子分解机层&#…

MinIO分片上传超大文件(纯服务端)

目录 一、MinIO快速搭建1.1、拉取docker镜像1.2、启动docker容器 二、分片上传大文件到MinIO2.1、添加依赖2.2、实现MinioClient2.3、实现分片上传2.3.0、初始化MinioClient2.3.1、准备分片上传2.3.2、分片并上传2.3.2.1、设置分片大小2.3.2.2、分片 2.3.3、分片合并 三、测试3…

Vscode+Pycharm+Vue.js+WEUI+django火锅(三)理解Vue

新创建的Vue项目里面很多文件&#xff0c;对于新手&#xff0c;老老实实做一下了解。 1.框架逻辑 框架的逻辑都是相通的&#xff0c;花点时间理一下就清晰了。 2.文件目录及文件 创建好的vue项目下&#xff0c;主要的文件和文件夹要先认识一下&#xff0c;并与框架逻辑对应起…

计算机网络803-(4)网络层

目录 1.虚电路服务 虚电路是逻辑连接 2.数据报服务 3.虚电路服务与数据报服务的对比 二.虚拟互连网络-IP网 1.网络通信问题 2.中间设备 3.网络互连使用路由器 三.分类的 IP 地址 1. IP 地址及其表示方法 2.IP 地址的编址方法 3.分类 IP 地址 &#xff08;1&#x…

使用 Go 和 Gin 框架构建简单的用户和物品管理 Web 服务

使用 Go 和 Gin 框架构建简单的用户和物品管理 Web 服务 在本项目中&#xff0c;我们使用 Go 语言和 Gin 框架构建了一个简单的 Web 服务&#xff0c;能够管理用户和物品的信息。该服务实现了两个主要接口&#xff1a;根据用户 ID 获取用户名称&#xff0c;以及根据物品 ID 获…

蓝桥杯【物联网】零基础到国奖之路:十七. 扩展模块之单路ADC和NE555

蓝桥杯【物联网】零基础到国奖之路:十七. 扩展模块之单路ADC和NE555 第一节 硬件解读第二节 CubeMx配置第三节 代码1&#xff0c;脉冲部分代码2&#xff0c;ADC部分代码![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/57531a4ee76d46daa227ae0a52993191.png) 第一节 …

EasyExcel读入数字类型数据时出现小数位丢失精度问题

这里写自定义目录标题 问题现象解决方案 问题现象 目前使用easyExcel读取导入文档时发现文档中的小数值4076204076.65会被读取为4076204076.6500001 尝试去查看了excel解压后的文件&#xff0c;发现这条数据在xml里存储的值就是4076204076.6500001&#xff0c;即是excel存储小…

利用 Python 爬虫采集 1688商品详情

1688是中国的一个大型B2B电子商务平台&#xff0c;主要用于批发和采购各种商品。对于需要从1688上获取商品详情数据、工程数据或店铺数据的用户来说&#xff0c;可以采用以下几种常见的方法&#xff1a; 官方API接口&#xff1a;如果1688提供了官方的API接口&#xff0c;那么可…

喜讯!迈威通信TSN产品通过“时间敏感网络(TSN)产业链名录计划”评测,各项指标名列前茅

TSN技术&#xff0c;作为推动企业网络化与智能化转型的关键力量&#xff0c;已成为工业网络迈向下一代演进的共识方向&#xff0c;正加速重构工业网络的技术架构与产业生态。为响应这一趋势&#xff0c;工业互联网产业联盟携手中国信息通信研究院及50余家产学研用单位&#xff…

使用Google开源工具gperftools进行堆内存占用分析

背景&#xff1a;项目中有多卡训练的需求&#xff0c;多进程时每个进程都需要编译&#xff0c;占用内存过大&#xff0c;需要找出内存占用多的点并尝试优化。 目标程序是python的多进程程序&#xff0c;torch_xla多卡训练&#xff0c;程序包含python及c库&#xff0c;尝试过其他…

精益生产现场管理和改善:从知识到实操的落地

在制造业的广阔天地中&#xff0c;精益生产作为一种追求浪费最小化、效率最大化的生产管理模式&#xff0c;已成为众多企业转型升级的关键路径。本文&#xff0c;深圳天行健企业管理咨询公司将从精益生产现场管理和改善的理论知识出发&#xff0c;深入探讨其从理念导入到实操落…

【重学 MySQL】四十七、表的操作技巧——修改、重命名、删除与清空

【重学 MySQL】四十七、表的操作技巧——修改、重命名、删除与清空 修改表添加字段语法示例注意事项 删除字段语法示例 修改字段使用 MODIFY COLUMN语法示例 使用 CHANGE COLUMN语法示例 重命名表语法示例 删除表语法示例 清空表使用 TRUNCATE TABLE使用 DELETE FROM对比 TRUNC…

pytest框架之fixture测试夹具详解

前言 大家下午好呀&#xff0c;今天呢来和大家唠唠pytest中的fixtures夹具的详解&#xff0c;废话就不多说了咱们直接进入主题哈。 一、fixture的优势 ​ pytest框架的fixture测试夹具就相当于unittest框架的setup、teardown&#xff0c;但相对之下它的功能更加强大和灵活。 …

宠物空气净化器该怎么选?希喂,小米、安德迈这三款好用吗?

不得不说&#xff0c;虽然现在购物网站的活动不少&#xff0c;可力度都好弱啊&#xff01;我想买宠物空气净化器很久了&#xff0c;觉得有点贵&#xff0c;一直没舍得入手。价格一直没变化&#xff0c;平台小活动根本没什么优惠&#xff0c;只能寄希望于双十一了&#xff0c;准…

【docker】要将容器中的 livox_to_pointcloud2 文件夹复制到宿主机上

复制文件夹 使用 docker cp 命令从容器复制文件夹到宿主机&#xff1a; docker cp <container_id_or_name>:/ws_livox/src/livox_to_pointcloud2 /path/to/host/folder sudo docker cp dandong_orin_docker:/ws_livox/src/livox_to_pointcloud2 /home

WPS的JS宏实现删除某级标题下的所有内容

想要删除Word文档中&#xff0c;包含特定描述的标题下所有内容&#xff08;包含各级子标题以及正文描述&#xff09;。 例如下图中&#xff0c;想删除1.2.1.19.1业务场景下所有内容&#xff1a; 简单版&#xff1a; 删除光标停留位置的大纲级别下所有的内容。实现的JS代码如下…

【YOLO学习】YOLOv2详解

文章目录 1. 概述2. Better2.1 Batch Normalization&#xff08;批归一化&#xff09;2.2 High Resolution Classifier&#xff08;高分辨率分类器&#xff09;2.3 Convolutional With Anchor Boxes&#xff08;带有Anchor Boxes的卷积&#xff09;2.4 Dimension Clusters&…

光伏开发:一充一放和两充两放是什么意思?

一充一放 一充一放是指储能设备在一次充电过程中充满电&#xff0c;并在一次放电过程中将电能全部释放。这种模式的原理相对简单&#xff0c;充电时电能转化为化学能或其他形式的能量储存&#xff0c;放电时则将这些能量转化回电能供应给负载。一充一放模式适用于对储能设备充…

2024年9月国产数据库大事记-墨天轮

本文为墨天轮社区整理的2024年9月国产数据库大事件和重要产品发布消息。 目录 2024年9月国产数据库大事记 TOP102024年9月国产数据库大事记&#xff08;时间线&#xff09;产品/版本发布兼容认证代表厂商大事记厂商活动相关资料 2024年9月国产数据库大事记 TOP10 2024年9月国…