【C】初阶数据结构3 -- 单链表

  之前在顺序表那一篇文章中,提到顺序表具有的缺点,比如头插,头删时间复杂度为O(n),realloc增容有消耗等。而在链表中,这些问题将得到解决。所以在这一篇文章里,我们将会讲解链表的定义与性质,以及最简单的链表 -- 单链表的结构,以及基础方法的实现。


目录

1  链表

  1) 链表的概念

 2) 结点(节点)

3) 链表结点的结构

4) 链表的性质 

 2  单链表

1) 单链表的头插、尾插

2) 单链表的头删、尾删

3) 单链表在指定位置之前、之后插入数据 


重点一  链表

1  链表

  1) 链表的概念

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

一个链表(这里举例为单链表)的逻辑结构如图所示:

 2) 结点(节点)

  从上面那个链表的结构的图来看,结点就是指上面那个链表中每个独立存在的一个方块,所以一个结点里面包含了两部分内容,一个是这个结点里面存储的数据域,另一个是指向在一个结点的指针域,用来找到下一个结点,然后这里的plist是一个指向链表中第一个结点的指针,里面存储的是第一个结点的地址,可以通过plist来找到后面的结点,从结点的结构来看,知道了plist,也就是链表第一个结点的地址,我们就可以顺着结点中的指针,依次找到每一个结点

3) 链表结点的结构

  上面知道了结点的定义,结点的结构就很容易实现了,就是一个保存结点数据的数据域,还有一个指向下一个结点的指针:

#typedef int ListDataType;
typedef struct ListNode
{ListDataType data;//结点数据struct ListNode* next;//指向下一个结点的指针
}ListNode;

4) 链表的性质 

(1) 由于链表每个结点都是独立的一块空间,结点之间是通过指针连在一起,所以链表在物理结构上不一定是连续的,但是在逻辑结构上是连续的,所以是属于线性表的(2) 每个结点是动态开辟的,而动态开辟的空间都是在内存里一个叫做堆区的空间开辟的(3) 由于每个结点是独立申请的,所以每个结点的物理地址不一定连续

 

重点二  单链表

 2  单链表

  单链表是链表的一种,其结点的结构就是上面那个图那样,每个结点里面就只有一个数据域,还有指向下一个结点的指针域,所以对于单链表来说,只能通过前面的结点找到后面的结点,是不能通过后面的结点找到后面的结点的

  单链表的结构:

typedef int SLTDataType;//singal list node -- 单链表结点typedef struct SListNode
{SLTDataType data;//结点的数据struct SListNode* next;//指向下一个结点的指针
}SLTNode;

  对于一个单链表来说,它的基础方法有链表的头插,头删,尾插,尾删,打印,查找,在指定位置之前插入数据,删除pos(这里的pos是一个节点的地址)结点,在指定位置之后插入数据,删除pos之后的节点, 销毁链表,同样,我们先讲解每一种方法的画图实现,之后再附上代码。


1) 单链表的头插、尾插

  既然是插入结点,那么就肯定会需要新开辟一个结点,而且只要插入结点就需要开辟新结点,所以可以把开辟新结点写成一个函数(buyNode),开辟新结点的功能也很容易实现,只要用malloc函数动态开辟出一个结点大小的空间,然后再让结点的data值赋值为想要开辟新结点的值,然后再让next指针为NULL,就开辟出了一个新结点。

  链表的头插的实现如图

但是我们在实现头插的过程中,发现phead(形参,链表的头节点)会改变指向,会从指向旧链表的头节点改为指向插入的新结点,要想让传过来的实参头插后链表新的首节点,这里必须改变实参(也就是原来链表头节点指针的指向,形参改变影响实参),所以这里必须传指针变量的地址,也就是二级指针

  尾插的实现也类似,如图

在第二步里面,需要通过头节点来找到尾节点,通过头节点找到尾节点的实现思路为:先创建一个节点指针变量 ptail 指向首结点,之后再让循环判断 ptail 的next指针为不为NULL,如果不为NULL,那就让ptail走向下一个结点(注意,这里判断条件不能是ptail为不为NULL,如何是这个判断条件,那么ptail就会走向NULL,后面势必会造成对NULL指针的解引用),代码为:

SLTNode* ptail = phead;
while (ptail->next != NULL)
{ptail = ptail->next;
}

  那么尾插是否需要传二级指针呢?

  其实不管是头插还是尾插,都需要考虑一种特殊情形,那就是链表为空的情况(就是指向头结点的指针为NULL),如果链表为空,上面的两种情况都会造成对空指针的解引用,所以如果链表为空,那就让指向头节点的指针指向新开辟的结点newnode就可以了,但是这里改变的只是形参,所以如果想要让实参也变为指向newnode的指针,就必须传指针的地址,也就是二级指针了。(这里如果看不懂的话,可以看下面的代码理解一下)。

  所以上面找尾节点的代码应该为:

//传过来的二级指针为SLTNode** pphead,指向头节点的指针为*pphead
SLTNode* ptail = *pphead;
while (ptail != NULL)
{ptail = ptail->next;
}

2) 单链表的头删、尾删

  既然是删除结点,那么就需要判断链表为不为空,也就是判断传过来的指向首节点的指针为不为NULL,如果为NULL,就不能删除结点。

  头删的过程如图:

头删也需要注意传递的参数也需要是二级指针(因为形参的改变要影响实参) 。

  尾删的过程如下:

寻找prev和ptail的代码如下:

//传过来的为二级指针**pphead
SLTNode* ptail = *pphead, *prev = NULL;
while (ptail->next != NULL)
{prev = ptail;ptail = ptail->next;
}

   头删和尾删的时候需要注意一种极端情况,那就是只有一个结点的情况((*pphead)->next == NULL),如果只有一个结点,那么prev指针就是NULL指针,那么就会出现NULL指针的解引用,但是对于头删的代码来说,就不会出现这种情况(可以结合下面的代码)。所以尾删只有一个结点的情况需要特殊处理,其实删除只有一个结点的链表尾删和头删都是一样的,所以链表只有一个结点时,尾删也就是头删


3) 单链表在指定位置之前、之后插入数据 

  同样的,既然是插入数据,就需要和头插、尾插一样开辟新的节点(这里与头插、尾插开辟新节点逻辑相同,不再赘述)。这里的位置指的是一个节点,这里叫做pos节点,那么实现逻辑如下图所示:

在pos节点之前插入数据

找到pos节点之前的prev节点的逻辑类似于尾插中找到尾节点,具体逻辑为:先将prev节点指定为首节点,如果prev的next指针不指向pos节点,那就让prev走到它的next指针,写成代码为:

//*pphead为首节点
SLTNode* prev = *pphead;while(prev->next != pos)
{prev = prev->next;
}

   我们来考虑一下特殊情况,那就是如果pos节点就是首节点的话(pos == *pphead),如果继续按照这个逻辑的话,prev节点是首届点,而pos节点也是首节点,那么prev的next指针始终不等于pos,所以最终会造成对NULL指针的解引用,所以如果pos是首节点,这时候需要特殊处理一下,仅需要调用一下头插的函数就可以了(可以结合下面代码思考一下)

在pos节点之后插入数据

   我们再来考虑一下特殊情况,就是如果pos是尾节点的情况,如果再按这个逻辑的话,会发现是没有问题的,不会出现对于NULL指针的解引用情况。但是有一个点需要注意,就是在第二步里面,一定要先让newnode的next指针先改变,再让pos的next指针改变,因为pos的next指针如果先改变的话,那就找不到pos的下一个节点了,且newnode会指向自己的。

  还有就是我们可以看到在pos节点之后插入数据,实现逻辑是没有用到首节点的,所以对于该函数实现的时候,只需要传pos参数和插入的数据 X 两个参数就可以了

  那么在指定位置之前和之后插入数据是否需要判断单链表是否为空呢?实际上是不需要的,因为在pos节点之前、之后插入数据就保证了该单链表是非空的,所以只需要判断pos节点是否为NULL就可以了。


4) 删除指定位置(pos)节点,删除指定位置(pos)之后的节点

  既然是删除节点,那就需要判断链表是否为空(判断逻辑与头删、尾删相同,不再赘述),这两个方法实现逻辑如下:

删除pos节点

  我们来考虑一下特殊情况,那就是如果pos节点是首节点、尾节点时。如果pos是尾节点,根据这个逻辑来实现的话,发现是没有问题的;但是如果pos是首节点,那么根据我们之前找prev节点的逻辑,会出现对NULL指针的解引用的,所以pos是首节点的情况需要特殊处理一下,只需要调用一下头删的代码就可以了。

删除pos节点之后的节点

  该方法的实现时有一个地方需要注意,就是要删除的节点,也就是pos的下一个节点不能为NULL,且该方法的实现只需要传递一个pos参数就可以了。


5) 打印、查找、销毁

  打印函数和查找函数的逻辑比较好实现,只要遍历整个单链表,然后打印每个节点的值或者比较节点的值要和查找的值相不相等就可以了,如果查找到了就返回对应节点的地址,如果没有查找到,就返回NULL。

  销毁链表也很简单,只要从首节点开始遍历,然后销毁每一个节点就可以了,只不过需要注意在释放当前节点之前,需要先把下一个节点的地址存起来,避免找不到下一个节点了。


6) 代码实现 

SList.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int SLTDataType;//singal list node -- 单链表结点typedef struct SListNode
{SLTDataType data;//结点的数据struct SListNode* next;//指向下一个结点的指针
}SLTNode;//打印
void SLTPrint(SLTNode* phead);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//头删
void SLTPopFront(SLTNode** pphead);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTDataType** pphead, SLTNode* pos, SLTDataType x);
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SLTDestroy(SLTNode** pphead);

SList.c文件:

#include"SList.h"//开辟结点的函数
SLTNode* buyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//开辟失败if (newnode == NULL){perror("malloc fail!\n");//打印错误信息exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}//打印
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);//传过来的不能是空指针if ((*pphead) == NULL){*pphead = buyNode(x);}else{SLTNode* newnode = buyNode(x);//让新开辟的结点next指针指向链表第一个结点newnode->next = *pphead;//再让指向第一个结点的指针指向新开辟的结点*pphead = newnode;}
}//头删
void SLTPopFront(SLTNode** pphead)
{//传过来的指针不能为空,并且链表不能为空assert(pphead && *pphead);//得先让首结点指向首结点的下一个结点,要不然会成野指针SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);//如果是空链表的话,就让指针指向新的结点if (*pphead == NULL){*pphead = buyNode(x);}//链表不为空,得先找到尾节点else{SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}//ptail为尾结点SLTNode* newnode = buyNode(x);ptail->next = newnode;}
}
//尾删
void SLTPopBack(SLTNode** pphead)
{//传过来的地址不能为NULL并且链表为空assert(pphead && *pphead);//如果链表中只有一个结点,那就是头删除if ((*pphead)->next == NULL){SLTPopFront(pphead);}else{//得先找到尾节点和尾节点的前一个结点SLTNode* ptail = *pphead;SLTNode* prev = NULL;while (ptail->next){prev = ptail;ptail = ptail->next;}prev->next = NULL;free(ptail);ptail = NULL;}
}//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{assert(phead);SLTNode* pcur = phead;while (pcur){if (pcur->data == x)return pcur;pcur = pcur->next;}return NULL;
}//在指定位置之前插入数据
void SLTInsert(SLTDataType** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);//如果pos节点就是首节点if (pos == *pphead){//头插SLTPushFront(pphead, x);}//如果pos不是首节点else{SLTNode* prev = *pphead;SLTNode* newnode = buyNode(x);while (prev->next != pos){prev = prev->next;}prev->next = newnode;newnode->next = pos;}
}//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);//如果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;}}//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = buyNode(x);//一定得先让newnode->next = pos->next//再让pos->next = newnodenewnode->next = pos->next;pos->next = newnode;
}//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{//pos的下一个结点不能为空,否则不能删除assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}//销毁链表
void SLTDestroy(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;//先存下一个结点SLTNode* next = pcur->next;while (pcur){next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}

test.c:

#include"SList.h"void Test()
{SLTNode* plist = NULL;测试头插/*SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPrint(plist);*///测试头删//SLTPopFront(&plist);/*SLTPopFront(&plist);SLTPopFront(&plist);SLTPopFront(&plist);*///SLTPopFront(&plist);//测试尾插SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);//测试尾删/*SLTPopBack(&plist);SLTPopBack(&plist);SLTPopBack(&plist);SLTPopBack(&plist);*///SLTPopBack(&plist);//测试查找/* SLTNode* pfind = SLTFind(plist, 10);if (pfind == NULL){printf("没找到\n");}else{printf("找到了\n");}*/SLTNode* pfind = SLTFind(plist, 1);if (pfind == NULL){printf("没找到\n");}else{printf("找到了\n");}//测试在指定位置之前插入数据//可以测试在1,2,4节点之前插入数据//SLTInsert(&plist, pfind, 5);//测试在指定节点之后插入数据//可以测试在1,2,4节点之后插入数据//SLTInsertAfter(pfind, 5);//测试删除pos节点//可以测试删除1,2,4节点//SLTErase(&plist, pfind);//测试删除pos之后的节点//可以测试删除1,2,3之后的节点//SLTEraseAfter(pfind);//可以使用调试测试销毁是否成功//如果销毁成功,下面打印结果就是NULLSLTDestroy(&plist);//打印SLTPrint(plist);
}int main()
{Test();return 0;
}

  单链表是链表里面结构最简单的一种链表,链表共分为8类(在双向链表里面会讲解),但是由于结构最简单,所以遍历只能从头开始遍历且在方法实现中涉及的细节点很多。同时,单链表中为什么传二级指针也需要自己理解。总之,如果刚开始学习单链表,并不会很容易理解;但是,一旦理解了,相信会对指针的理解更加深刻。

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

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

相关文章

网络网络层ICMP协议

网络网络层ICMP协议 1. ICMP 协议介绍 ICMP&#xff08;Internet Control Message Protocol&#xff09;是 TCP/IP 协议簇中的网络层控制报文协议。用于在 IP 主机、路由器之间传递控制消息&#xff0c;提供可能有关通信问题的反馈信息。 以及用于网络诊断或调试&#xff08;…

nvm 管理nodejs,安装pnpm后报错,出现:pnpm不是内部或外部命令,也不是可运行的程序或批处理文件。

系统环境&#xff1a;window11&#xff0c;exe安装版nvm出现的该问题&#xff0c;&#xff08;如果是解压缩配置版本&#xff0c;环境变量自己配置&#xff0c;可能就不会出现这个问题了&#xff09; 注意&#xff1a;安装nvm时&#xff0c;两个路径尽量放到一个盘上&#xff…

论文阅读:Searching for Fast Demosaicking Algorithms

今天介绍一篇有关去马赛克的工作&#xff0c;去马赛克是 ISP 流程里面非常重要的一个模块&#xff0c;可以说是将多姿多彩的大千世界进行色彩还原的重要一步。这篇工作探索的是如何从各种各样的去马赛克算法中&#xff0c;选择最佳的一种。 Abstract 本文提出了一种方法&…

活动预告 | CCF开源发展委员会开源供应链安全技术研讨会(2025第一期)——“大模型时代的开源供应链安全风控技术”...

点击蓝字 关注我们 CCF Opensource Development Committee CCF开源发展委员会开源供应链安全工作组&#xff08;以下简称CCF-ODC-OSS&#xff09;将于1月17日下午在北京黄大年茶思屋举行2025年第一期开源供应链安全技术研讨会&#xff0c;此次研讨会主题为“大模型时代的开源供…

centos 8 中安装Docker

注&#xff1a;本次样式安装使用的是centos8 操作系统。 1、镜像下载 具体的镜像下载地址各位可以去官网下载&#xff0c;选择适合你们的下载即可&#xff01; 1、CentOS官方下载地址&#xff1a;https://vault.centos.org/ 2、阿里云开源镜像站下载&#xff1a;centos安装包…

关于Profinet 从站转 EtherNet/IP 从站网关详细说明

一、产品概述 1.1 产品用途 本产品是 PN(Profinet) 和 EtherNet/IP 网关&#xff0c;使用数据映射方式工作。 本产品在 PN 侧作为 PN IO 从站&#xff0c;接 PN 主站设备&#xff0c;比如西门子 PLC 等&#xff1b;在EtherNet/IP 侧做为 EtherNet/IP 从站&…

【SH】Xiaomi9刷Windows10系统研发记录 、手机刷Windows系统教程、小米9重装win10系统

文章目录 参考资料云盘资料软硬件环境手机解锁刷机驱动绑定账号和设备解锁手机 Mindows工具箱安装工具箱和修复下载下载安卓和woa资源包第三方Recovery 一键安装Windows准备工作创建分区安装系统 效果展示Windows和Android一键互换Win切换安卓安卓切换Win 删除分区 参考资料 解…

企业服务-团队协作相关平台极简介绍

前言 最近&#xff0c;为一家企业做咨询&#xff0c;该公司主要从事地产行业&#xff0c;老板李总招了几名研发人员&#xff0c;想着开发自己的行业APP&#xff0c;但是3年了&#xff0c;产品一直拿不出手&#xff0c;按李总的说法&#xff0c;产品还是很不成熟&#xff0c;但…

怎么防止SQL注入攻击

引言 SQL注入攻击是黑客对数据库进行攻击的常用手段之一&#xff0c;随着B/S模式应用开发的发展&#xff0c;使用这种模式编写应用程序的程序员也越来越多。但是由于程序员的水平及经验参差不齐&#xff0c;相当大一部分程序员在编写代码的时候&#xff0c;没有对用户输入数据…

一文说清楚Linux gdb

以下是关于 GDB&#xff08;GNU Debugger&#xff09; 的详细介绍&#xff1a; 什么是 GDB&#xff1f; 定义 GDB&#xff08;GNU Debugger&#xff09;是 GNU 项目开发的一款功能强大的调试工具&#xff0c;用于调试 C、C、Fortran 等语言编写的程序。它允许开发者执行程序时…

api开发及运用小红书笔记详情api如何获取笔记详情信息

item_get_video-获得某书笔记详情 smallredbook.item_get_video 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地址中&#xff09;[item_search,i…

蓝桥杯第二天学习笔记

二维码生成&#xff1a; import qrcode from PIL import Image, ImageDraw, ImageFont import osdef generate_custom_qr_code(data, qr_file_path, logo_file_pathNone, textNone):# 创建QRCode对象qr qrcode.QRCode(version1,error_correctionqrcode.constants.ERROR_CORRE…

Springboot和Es整合

说明&#xff1a;本文章主要是简单整合和简单增删改查。 1.pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi…

stack_queue的底层,模拟实现,deque和priority_queue详解

文章目录 适配器Stack的模拟实现Queue的模拟实现vector和list的对比dequedeque的框架deque的底层 priority_queuepriority_queue的使用priority_queue的底层仿函数的使用仿函数的作用priority_queue模拟实现 适配器 适配器是一种模式&#xff0c;这种模式将类的接口转化为用户希…

基于Python机器学习、深度学习技术提升气象、海洋、水文领域实践应用-以ENSO预测为例讲解

1. 背景与目标 ENSO&#xff08;El Nio-Southern Oscillation&#xff09;是全球气候系统中最显著的年际变率现象之一&#xff0c;对全球气候、农业、渔业等有着深远的影响。准确预测ENSO事件的发生和发展对于减灾防灾具有重要意义。近年来&#xff0c;深度学习技术在气象领域…

网络安全概述

在早期的互联网&#xff08;也是一种计算机网络&#xff09;中数据都是明文传输的&#xff0c;例如直接使用http协议。但由于越来越多的商业和政府的数据也都在互联网传输&#xff0c;直接使用明文传输&#xff0c;相当于让数据在网络中裸奔&#xff0c;而且网络中攻击者可以直…

39.【4】CTFHUB web sql 布尔注入

进入靶场 按照提示输入1 布尔注入只显示正确与否&#xff0c;手动注入太麻烦,用sqlmap -dbs爆出库名 -tables爆出表名 -columns 爆出字段名 --dump得到flag 笔记 1&#xff0c;sqlmap使用步骤 -dbs 爆出表名 -tables爆出库名 -columns爆出字段名 --dump爆出字段内容 2&a…

C#中通道(Channels)的应用之(生产者-消费者模式)

一.生产者-消费者模式概述 生产者-消费者模式是一种经典的设计模式&#xff0c;它将数据的生成&#xff08;生产者&#xff09;和处理&#xff08;消费者&#xff09;分离到不同的模块或线程中。这种模式的核心在于一个共享的缓冲区&#xff0c;生产者将数据放入缓冲区&#x…

【STM32】HAL库USB实现软件升级DFU的功能操作及配置

【STM32】HAL库USB实现软件升级DFU的功能操作及配置 文章目录 DFUHAL库的DFU配置修改代码添加条件判断和跳转代码段DFU烧录附录&#xff1a;Cortex-M架构的SysTick系统定时器精准延时和MCU位带操作SysTick系统定时器精准延时延时函数阻塞延时非阻塞延时 位带操作位带代码位带宏…

kotlin的dagger hilt依赖注入

依赖注入&#xff08;dependency injection, di&#xff09;是设计模式的一种&#xff0c;它的实际作用是给对象赋予实例变量。 基础认识 class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceSta…