C语言进阶——数据结构之链表

前言

hello,大家好呀,我是Humble  在之前的两篇博客,我们学完了数据结构中的顺序表,还对它进行了一个应用,做了一个通讯录的小项目

那今天我们再来学习一个新的数据结构——链表

b90abe6962934bd1880f3d53f5b63113.jpg

引入

我们来回忆一下顺序表

对于顺序表,我们发现它有下面的这些问题

1.中间/头部的插入删除,时间复杂度为O(N)
2.增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗
3.增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间

思考:如何解决以上问题呢?有没有以一种数据结构,它可以解决顺序表的这些问题呢?

这就是我们今天要讲的链表
 

链表的概念及结构

链表在物理存储结构上是非连续、非顺序的存储的、

其数据元素的逻辑顺序是通过链表中的指针链接次序实现的

而与顺序表不同的是,链表是由节点组成的
节点的组成主要有两个部分:

1.当前节点要保存的数据

2.保存下一个节点的地址(指针变量)
 

变量来保存下一个节点位置才能从当前节点找到下一个节点


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

struct SListNode
{
int data; //节点数据,我们假设当前保存的节点为整型
struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数
据,也需要保存下一个节点的地址


所以,当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一 个节点的地址就可以了(有点绕,请耐心理解哦)

那么,给定的链表结构中,我们来实现一下节点从头到尾的打印吧~

我们在创建一个SList 的工程表示单链表

然后创建3个文件,分别是我们的SList.h 头文件 ,SList.c源文件以及测试文件test.c

(这个大家应该已经很熟悉了吧)

在三个文件中,我们分别去实现各自的职能

SList.h

#pragma oncetypedef int SLTDataType;typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;void SLTPrint(SLTNode* phead);//打印

SList.c

#include"SList.h"void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}

test.c

#include "SList.h"void SlistTest01() {//一般我们不会这样去创建链表,这里只是为了给大家展示链表的打印SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));node1->data = 1;SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));node2->data = 2;SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));node3->data = 3;SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;  SLTNode* plist = node1;SLTPrint(plist);  //打印1->2->3->4->NULL
}int main()
{SlistTest01();return 0;
}

我们来测试一下,按照我们的想法,应该打印1->2->3->4->NULL

运行结果:


 

单链表的实现

找到了链表的打印,我们就来实现链表的各个功能吧

链表的尾插

这要分两种情况来讨论

1.链表不为空

2.链表为空

先画张图来辅助理解一下:

假设我们要在链表插入 元素4

下面我们来写尾插STLPushBack的代码:

void SLTPushBack(SLTNode** pphead, SLTDataType x) //注意这里pphead是二级指针,用**
{assert(pphead);SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));newnode->data = x;newnode->next = NULL;//链表为空,新节点作为pheadif (*pphead == NULL) {*pphead = newnode;return;}//链表不为空,找尾节点SLTNode* ptail = *pphead;while ((ptail->next) != NULL) //遍历{ptail = ptail->next;}//遍历完之后ptail就是尾节点ptail->next = newnode; //完成尾插
}

下面我们来测试一下

我们在test.c中这样写:

void SlistTest02()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1); //我们要把plist指针的地址传过去,这个很重要!SLTPrint(plist); //预计结果1->NULL
}int main()
{SlistTest02();return 0;
}

运行一下:

当然,因为我们下面的操作都要设计申请节点,每次都要写:

   SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    newnode->data = x;
    newnode->next = NULL;

我们干脆就再写一个函数,之后直接调用就行

这样代码就会变成这样

SLTNode* SLTBuyNode(SLTDataType x) //申请新节点
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));newnode->data = x;newnode->next = NULL;return newnode;}void SLTPushBack(SLTNode** pphead, SLTDataType x) 
{assert(pphead);SLTNode* newnode = SLTBuyNode(x);//链表为空,新节点作为pheadif (*pphead == NULL) {*pphead = newnode;return;}//链表不为空,找尾节点SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}//ptail就是尾节点ptail->next = newnode;
}

接下来我们来看一下头插SLTPushFront:

它同样分2种情况,但它们的代码是一样的,所以就不用分了

void SLTPushFront(SLTNode** pphead, SLTDataType x) 
{assert(pphead);SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}

测试一下:
 

void SlistTest02()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPushFront(&plist, 5);          SLTPushFront(&plist, 6);        SLTPushFront(&plist, 7);SLTPrint(plist);         //期望结果为:7->6->5->1->2->3->4->NULL
}int main()
{SlistTest02();return 0;
}

运行结果如下:

接下来看一下尾部删除SLTPopBack吧~

既然要删除,我们要保证链表不为空,所以相比前面的这几种操作,它还要加上

assert(*pphead);//表示链表不能为空

此外,要分链表是否只有一个节点,即是否有前驱节点这2中情况
 

void SLTPopBack(SLTNode** pphead) 
{assert(pphead);assert(*pphead);//保证链表不能为空//链表只有一个节点if ((*pphead)->next == NULL) {free(*pphead);*pphead = NULL;return;}//链表有多个节点SLTNode* ptail = *pphead;SLTNode* prev = NULL;while ((ptail->next)!=NULL){prev = ptail;ptail = ptail->next;}prev->next = NULL;//销毁尾结点free(ptail);ptail = NULL;
}

我们也来测试一下:

void SlistTest02()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPopBack(&plist);SLTPrint(plist);  //预期结果为1->2->3->NULL}int main()
{SlistTest02();return 0;
}

运行结果如下:

接下来看一下头部删除SLTPopFront吧~

这个也很简单,我们直接上代码~

//头删
void SLTPopFront(SLTNode** pphead) 
{assert(pphead);//链表不能为空assert(*pphead);//让第二个节点成为新的头//把旧的头结点释放掉SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}

接下来我们也是测试一下

void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);//头删SLTPopFront(&plist);SLTPrint(plist);  //2->3->4->NULLSLTPopFront(&plist);SLTPrint(plist);  //3->4->NULL
}int main()
{SlistTest03();return 0;
}

运行结果:
 

 

好,我们已经实现了头部和尾部的插入和删除的操作,接下来我们来实现一下查找的操作~

//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x)
{assert(pphead);//遍历链表SLTNode* pcur = *pphead;while (pcur) //等价于pcur != NULL{if (pcur->data == x) {return pcur;}pcur = pcur->next;}//没有找到return NULL;}

接下来测试一下:
 

void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTNode* FindRet = SLTFind(&plist,1); //1 在链表中,可以找到if (FindRet) {printf("找到了!\n");}else {printf("未找到!\n");}}int main()
{SlistTest03();return 0;
}

运行结果:

接下来我们看一下在指定位置插入数据~

它分为2种,在指定位置之前插入和在指定位置之后插入数据

先看在指定位置之前插入数据

它要分要插入的位置是头节点和不是头节点2种情况讨论哦

实现代码如下:
 

//在指定位置之前插入数据void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(*pphead);//链表 不能为空!assert(pos);SLTNode* newnode = SLTBuyNode(x);//pos刚好是头结点if (pos == *pphead) {//头插SLTPushFront(pphead, x);return;}//pos不是头结点的情况SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = newnode;newnode->next = pos;}

好,我们来测试一下~

void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTNode* FindRet = SLTFind(&plist,1);SLTInsert(&plist, FindRet, 100); SLTPrint(plist);//预期是100->1->2->3->4->NULL}int main()
{SlistTest03();return 0;
}

运行结果:
 

接下来我们再看一下在指定位置之后插入数据SLTInsertAfter吧~

这个实现起来要比在指定位置之前插入要简单

我们看代码:
 

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;  //特别注意一下这里的顺序哦~pos->next = newnode;}

写完后也测试一下:
 


void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTNode* FindRet = SLTFind(&plist, 1);SLTInsertAfter(FindRet, 100); SLTPrint(plist);//预期是1->100->2->3->4->NULL}int main()
{SlistTest03();return 0;
}

测试一下:

那么,插入讲完了,我们接下来再看一下删除操作

分别是删除pos节点以及删除pos之后的节点

先看一下删除pos节点  的情况吧~

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(*pphead);assert(pos);//pos刚好是头结点,没有前驱节点,执行头删if (*pphead == pos) {//头删SLTPopFront(pphead);return;}//pos不是头结点SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}

下面来测试一下:

void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTNode* FindRet = SLTFind(&plist, 4);SLTErase(&plist, FindRet);SLTPrint(plist);//预期是1->2->3->NULL}int main()
{SlistTest03();return 0;
}


 

运行结果:

再看一下删除pos之后的节点吧~

下面是实现的代码~

//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{assert(pos);//pos->next不能为空assert(pos->next);SLTNode* del = pos->next;  //定义一个中间的变量用来保存pos->next = pos->next->next;free(del);del = NULL;}

下面进行测试:
 

void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTNode* FindRet = SLTFind(&plist, 2);SLTEraseAfter(FindRet);SLTPrint(plist);//预期是1->2->4->NULL}int main()
{SlistTest03();return 0;
}

好,最后我们来看一下链表的销毁操作吧~

//销毁链表
void SListDesTroy(SLTNode** pphead)
{assert(pphead);assert(*pphead);SLTNode* pcur = *pphead; //pur依旧是作为临时变量,用于保存~while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;}

关于链表的销毁,我们可以通过调试来观察,这里就不再演示了,大家可以自己测试一下~

好,到这,我们就把单链表的实现给讲完了~(鼓掌鼓掌)

好,那么这里又出现了一个新的问题,我们在这里花了这么多精力说了单链表的各种操作,那么链表究竟有多少种类呢?它与单链表又是什么关系呢?

接下来,我们就来说说链表的分类

链表的分类

不知道大家有没有想过为什么我创建的这个工程名为SList?

其实它是Single Linked list 的简写,也就是单链表的意思

我们上面的对链表的各种插入,删除都是对单链表进行操作的

那其实 链表的种类有很多,单链表的全称就是不带头单向不循环链表

我们在平时为了方便就称为单链表了~

既然有不带头就有带头的,由单向也就有双向的,有不循环的也就有循环的

如此这般三三组合,其实就可以推出链表的种类有2*2*2=8种

各个种类的关系如图:

看到这么多种类的链表,大家也不要太焦虑,去想单单一种类型的单链表就学了这么久,更何况还有7种.....

其实,我们实际中最常用只有两种结构:单链表带头双向循环链表(简称双向链表),后者我们会在之后的博客中进行介绍与分享的~

最后我们在来看一下单链表双向链表各自的一些特点吧~
1.单链表(不带头单向不循环链表):结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等

这种结构也是在笔试面试中出现很多


2.双向链表(带头双向循环链表):结构最复杂,一般用在单独存储数据

实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,所以实现反而简单了,这个我们代码实现了就知道了,这里只要先大致有一个印象就行,不必担心~

结语

好了,今天关于链表的分享就到这里了

在学习编程的道路上Humble与各位同行,加油吧各位!

最后希望大家点个免费的赞或者关注吧(感谢感谢),也欢迎大家订阅我的专栏

让我们在接下来的时间里一起成长,一起进步吧!

1d8bd2383fe54a7aa576bdd8d41dc462.png

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

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

相关文章

小程序技术实践:快速开发适配鸿蒙的App

今年,在中国,被各大媒体和开发者称为“鸿蒙元年”。 在2023年底就有业内人士透露,华为明年将推出不兼容安卓的鸿蒙版本,未来IOS、鸿蒙、安卓将成为三个各自独立的系统。 果不其然,执行力超强的华为,与202…

1、【vue篇】vue框架快速上手

注意事项&#xff1a; methods必须要加s 导入vue&#xff1a;<script src"https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>导入Axios:<script src"https://unpkg.com/axios/dist/axios.min.js"></script> 简单Vue程序…

轻松上手:通过阿里云PAI QuickStart微调部署Qwen-72B-Chat模型

作者&#xff1a;熊兮、求伯、一耘 引言 通义千问-72B&#xff08;Qwen-72B&#xff09;是阿里云研发的通义千问大模型系列的720亿参数规模模型。Qwen-72B的预训练数据类型多样、覆盖广泛&#xff0c;包括大量网络文本、专业书籍、代码等。Qwen-72B-Chat是在Qwen-72B的基础上…

新年钜惠|泰迪智能科技免费协助企业完成3个行业AI案例建模

在新年到来之际&#xff0c;为了感谢各企业一直以来对泰迪智能科技的支持&#xff0c;日前我们推出了新年钜惠活动即&#xff1a;免费协助企业完成3个行业AI案例建模。我们希望通过这样的活动&#xff0c;可以帮助更多的企业迈出AI应用的第一步&#xff0c;同时我们也希望能够为…

如何在阿里云提交使用工单

有时候大家在使用阿里云的服务时候&#xff0c;可能会遇到一些问题&#xff0c;或许是云服务器如何升级了如何改套餐啊之类的&#xff0c;亦或者是域名ICP备案啊看进度啊等等问题&#xff0c;遇到问题怎么办不要慌。我们可以使用阿里云的工单系统&#xff0c;阿里云工单系统可以…

Linux命令大全

文章目录 目录操作与文件管理系统信息与管理软件包管理和系统维护压缩与解压缩网络与通信辅助工具与信息获取文本处理与搜索时间与日期操作网络连接与通信&#xff08;补充&#xff09;链接管理磁盘与存储管理环境变量与路径设置用户和组管理查看系统信息 当然&#xff0c;以下…

Python使用pip命令安装外部库-项目内安装外部库-全局安装外部库

一、前言 在进行Python项目开发时需要安装一些外部库来扩展项目功能&#xff0c;因此需要了解pip命令的详细使用。 二、基本语法 1.安装库 pip install 包名 2.安装特定版本 pip install 包名版本号 3.升级库 pip install --upgrade 包名 4.卸载库 pip uninstall 包名 5.查看已…

搜索与图论第七期 Prime算法

前言 prime算法也是一种图的结构 &#xff0c;哎图的结构还有好多好多&#xff0c;大家慢慢学吧&#xff01;&#xff01;&#xff01; 一、prime 算法的基本内容 过程&#xff1a; 最小生成树prim算法简单理解他的寻找路径的过程&#xff0c;从一个顶点V0开始&#xff0c;…

字节跳动 ByteHouse 云原生之路 – 计算存储分离与性能优化

01 起源 ByteHouse 的故事从字节跳动对于先进数据处理和分析的需求开始&#xff0c;这一需求随着公司业务规模的迅速扩张而日益增长&#xff0c;起源是对开源数据库管理系统 ClickHouse 的改造和增强。面对数据处理的高延迟、大规模数据操作的复杂性以及数据存储和处理成本的上…

【SGX系列教程】(一)Intel-SGX SDK在ubuntu22.04下安装全流程

文章目录 一.概述1.1 SGX三大组件1.2 SGXDataCenterAttestationPrimitives 二.安装流程2.1 检查服务器是否支持SGX2.2 sgx硬件/软件开启方法2.3 sgx dirver驱动安装&#xff1b;2.3.1 linux-sgx-driver驱动程序2.3.2 Intel SGX Support in the Linux Kernel&#xff08;linux内…

线程的同步和互斥学习笔记

目录 互斥锁的概念和使用 线程通信-互斥 互斥锁的创建和销毁 申请锁-pthread_mutex_lock 释放锁-pthread_mutex_unlock 读写锁的概念和使用 死锁的避免 互斥锁的概念和使用 线程通信-互斥 临界资源 一次只允许一个任务&#xff08;进程、线程&#xff09;访问的共享资…

电脑有网,浏览器连不上网,其他应用却能用

当我们访问浏览器的时候显示&#xff0c;你尚未链接&#xff0c;代理服务器可能有问题&#xff0c;或地址不正确的时候.可你的wifi任然是连接的&#xff0c;但是只有浏览器用不了&#xff0c;微信和其他程序都可以正常连接&#xff0c;这是为什么呢&#xff1f; 绝大多数是因为…

K8S的HPA

horiztal Pod Autoscaling&#xff1a;pod的水平自动伸缩&#xff0c;这是k8s自带的模块&#xff0c;它是根据Pod占用cpu比率到达一定的阀值&#xff0c;会触发伸缩机制 Replication controller 副本控制器&#xff1a;控制pod的副本数 Deployment controller 节点控制器&…

云原生网关哪家强:Sealos 网关血泪史

作者&#xff1a;Sealos 创始人&#xff0c;环界云计算 CEO 方海涛 Sealos 公有云 &#xff08;https://cloud.sealos.io&#xff09; 几乎打爆了市面上所有主流的开源网关&#xff0c;本文可以给大家很好的避坑&#xff0c;在网关选型方面做一些参考。 Sealos Cloud 的复杂场…

jenkins安装配置,使用Docker发布maven项目全过程记录(1)

使用的CentOS8 系统&#xff0c;其它Linux系统类似 1、jenkins安装 1.1、配置JAVA环境 使用的服务器环境中&#xff0c;安装了Java1.8与Java17&#xff0c;当前jenkins的最低java版本要求java11。系统默认使用的是java1.8&#xff0c;因此需要切换环境。 alternatives --co…

Qt采集本地摄像头推流成rtsp/rtmp(可网页播放/支持嵌入式linux)

一、功能特点 支持各种本地视频文件和网络视频文件。支持各种网络视频流&#xff0c;网络摄像头&#xff0c;协议包括rtsp、rtmp、http。支持将本地摄像头设备推流&#xff0c;可指定分辨率和帧率等。支持将本地桌面推流&#xff0c;可指定屏幕区域和帧率等。自动启动流媒体服…

flink-java使用介绍,flink,java,DataStream API,DataSet API,ETL

1、环境准备 文档&#xff1a;https://nightlies.apache.org/flink/flink-docs-release-1.17/zh/ 仓库&#xff1a;https://github.com/apache/flink 下载&#xff1a;https://flink.apache.org/zh/downloads/ 下载指定版本&#xff1a;https://archive.apache.org/dist/flink…

Spring复习-问题回答

1.什么是 spring&#xff0c;你对 spring 的理解? Spring是一个轻量级&#xff0c;非侵入式的&#xff08;不使用框架特定的类&#xff0c;感受不到框架&#xff09;IOC和AOP一站式的java后端开发框架&#xff0c;简化企业开发。 2.spring 的优缺点 优点&#xff1a; Spr…

STL第二讲

第二讲 视频标准库源码版本&#xff1a;gnu c 2.9.1/4.9/Visual C OOP vs GP GP是将datas与methods分开&#xff0c;OOP相反&#xff1b; 为什么list不能使用全局的sort&#xff1f; 因为sort源代码&#xff1a; *(first (last - first)/2) // 此迭代器只能是随机访问迭代…

使用 Python 数据写入 Excel 工作表

在数据处理和报告生成等工作中&#xff0c;Excel 表格是一种常见且广泛使用的工具。然而&#xff0c;手动将大量数据输入到 Excel 表格中既费时又容易出错。为了提高效率并减少错误&#xff0c;使用 Python 编程语言来自动化数据写入 Excel 表格是一个明智的选择。Python 作为一…