当前位置: 首页 > news >正文

C语言数据结构(3)----无头单向非循环链表

目录

1. 链表的概念及结构

2. 链表的分类

3. 无头单向非循环链表的实现(下面称为单链表)

3.1 SListNode* BuySListNode(SLTDateType x) 的实现

 3.2 void SListPrint(SListNode* plist) 的实现

3.3 void SListPushBack(SListNode** pplist, SLTDateType x) 的实现

3.4 void SListPushFront(SListNode** pplist, SLTDateType x) 的实现

3.5 void SListPopBack(SListNode** pplist) 的实现

3.6 void SListPopFront(SListNode** pplist) 的实现

3.7 SListNode* SListFind(SListNode* plist, SLTDateType x) 的实现

3.8 void SListInsertAfter(SListNode* pos, SLTDateType x) 的实现

3.9 void SListEraseAfter(SListNode* pos) 的实现

3.10 void SListDestroy(SListNode* plist) 的实现

4. 题目练习

4.1 链表中的倒数第 K 个节点

4.2 环形链表 Ι && 环形链表 Ⅱ

4.3 反转链表

4.4 合并两个有序链表


1. 链表的概念及结构

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

 链表的逻辑结构其实和上面的通过连接头连接起来的火车车厢差不多。

 注意:

1:从上图中可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续(程序中表现为每个节点在堆上的内存是不连续的)

2:现实中的节点一般都是从堆上申请出来的。

3:从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。

2. 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向

 2. 带头或者不带头

 3. 循环和不循环

八种结构是怎么来的呢?需要数学基础哈,这里就不多说了。组合一下!!

虽然说链表有这么多中,但是我们最常用的是两种哈!

1:无头单向非循环链表。

2:带头双向循环链表。

 1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。

3. 无头单向非循环链表的实现(下面称为单链表)

// slist.h
typedef int SLTDateType;
typedef struct SListNode
{SLTDateType data;struct SListNode* next;
}SListNode;// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode* plist);

3.1 SListNode* BuySListNode(SLTDateType x) 的实现

为啥要将开辟一个新节点封装成函数嘞,因为在单链表的尾插,头插,指定位置后面插入均需要开辟新的节点,如果我们将开辟节点封装成一个函数,就可以少写几行代码,调用函数就阔以啦!

参数列表中的 x 用来初始化向堆区申请的节点。

//动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{//开辟节点SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));//返回的指针可能为空,判断一下if (newNode == NULL){perror("BuyNewNode::malloc");exit(-1);}else{//将值初始化,newNode->next初始化为NULL是很有必要的//可以通过后面对该函数的使用来体会。newNode->data = x;newNode->next = NULL;}
}

 3.2 void SListPrint(SListNode* plist) 的实现

参数 plist 是指向第一个结点的指针哈!

这里就有一个小小的问题,在写顺序表的代码时,我们常常对传入的指针进行断言,这里呢到底需不需要断言 plist 指针呢?这里是没有必要的哈,根据顺序表的结构,传入的指针指向的结构体里面存储的是 堆上开辟的数组的指针,如果传入的指针为空,我们尝试去访问顺序表中的数据,就会发生空指针的解引用引起程序崩溃!

 再看链表,一个节点就是一个数据,当 plist 传入空指针时,就说明了此时单链表为空,直接打印一个NULL就行,或者啥都不打印。故不需要断言 plist 指针。

总结:断言可以确保指针的合法使用,如果程序可能出现非法使用指针,则需要断言,不妨称之为暴力检查;或者用 if 进行判断,不妨称之为温柔的检查。不知道 uu 们喜欢哪一种呢?

打印数据就创建一个指针 cur,初始化为 plist,用 cur 指针遍历链表中的数据,直到 cur 为空为止。

//打印单链表
void SListPrint(SListNode* plist)
{//初始化cur指针SListNode* cur = plist;//循环遍历while (cur != NULL){//生动的显示链表的结构打印一个 ->printf("%d->", cur->data);cur = cur->next;}//链表的尾节点指向 NULLprintf("NULL");printf("\n");
}

3.3 void SListPushBack(SListNode** pplist, SLTDateType x) 的实现

这里需要注意的一个点就是为啥传二级指针哈(C++传一级指针加引用也行的哦)。了解了函数栈帧的创建和销毁之后印象最深刻的结论就是这个吧:形参只是实参的临时拷贝,形参的改变不会影响实参。

如果有不理解的地方请参考:http://t.csdn.cn/SYcAp

好的,理解了这个我们就来尝试理解为啥要传二级指针哈。

假设我们传入的是一级指针:当我们的链表为空时,指向第一个节点的指针 plist 就是 NULL,这时我们将 plist 传入 SListPushBack 函数,尾插嘛,需要新节点,调用上面的 BuySListNode 函数即可。既然插入了一个节点,当然是需要改变 plist 的值的。我们直接将该函数返回的新节点的指针赋值给 plist,这能达到改变 plist 的目的吗?

显然是不能的。下图是传入一级指针尾插数据打印后的结果:

这是为啥呢?

 一级指针不行为啥二级指针就行嘞?

 那好,在弄清楚了这个问题,我们就可以得出结论:当我们需要改变 plist 的值时就必须传入plist的地址。

尾插还会分两种情况,当单链表中没有节点时,我们只需要将 newNode 赋值给 *pplist,如果说有节点的话,就需要找到单链表中的尾节点 tail,然后令 tail -> next = newNode即可。还是比较好理解的哈。真正难理解的是二级指针那里呢!!

//单链表的尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{assert(pplist);SListNode* newNode = BuySListNode(x);//链表中没有节点,直接赋值即可if (*pplist == NULL){*pplist = newNode;}else{//这里是找尾节点SListNode* tail = *pplist;while (tail->next != NULL){tail = tail->next;}//连接新的节点tail->next = newNode;}
}

3.4 void SListPushFront(SListNode** pplist, SLTDateType x) 的实现

头插和尾插差不多的哦,头插同样要改变 plist 的值所以也需要传 plist 的地址哦!头插就简单啦:直接让 newNode -> next = *pphead 就行,链表的增删改查函数,你就先想一般的情况,然后嘞在看看有没有特殊的情况,比如说:链表为空,只有一个节点啥的呀,这样就能分析出需不需要把一种情况单独拿出来处理。显然这个头插函数是不需要的哦!

//单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{assert(pplist);SListNode* newNode = BuySListNode(x);newNode->next = *pplist;*pplist = newNode;
}

3.5 void SListPopBack(SListNode** pplist) 的实现

尾删的话,肯定没有数据的话是不让删的,需要断言 *pplist。同样可能改变 plist 需要传入 plist 的地址哦!

尾删有两种思路的哦!

1:双指针

 根据上面说的链表函数接口的写法,我们把一般的情况写出来了,就得看看有没有特殊的情况捏,显然是有的哦,当链表只有一个节点时,这个程序会崩溃的!当只有一个节点时         (*plplist)->next 就是空,无法进入循环,prev也就是 NULL,prev->next = NULL,这行代码就会发生空指针的解引用,程序会崩溃的!所以需要单独处理只有一个节点的情况。

void SListPopBack(SListNode** pplist)
{assert(*pplist);assert(pplist);if ((*pplist)->next == NULL){free(*pplist);*pplist = NULL;}else{SListNode* tail = *pplist;SListNode* prev = NULL;while (tail->next != NULL){prev = tail;tail = tail->next;}free(tail);prev->next = NULL;}
}

2:不找到尾节点,找尾节点的前一个节点

 这里同样是需要单独处理一种情况的喔!

void SListPopBack(SListNode** pplist)
{assert(*pplist);assert(pplist);//只有一个节点的情况if ((*pplist)->next == NULL){free(*pplist);*pplist = NULL;}else{//骑士不是找尾节点啦,找的是尾节点的前一个节点SListNode* tail = *pplist;while (tail->next->next != NULL){tail = tail->next;}//释放,置空free(tail->next);tail->next = NULL;}
}

3.6 void SListPopFront(SListNode** pplist) 的实现

头删肯定要改变 plist 的撒,所以要传 plist 的地址哦,同样需要断言,没有数据不允许删除数据。

头删就简单啦,找到 *pplist 的下一个节点,记录下来,然后释放 *pplist,将 *pplist 置为记录下来的那个值就行。

//单链表的头删
void SListPopFront(SListNode** pplist)
{assert(pplist);assert(*pplist);//记录*pplist的下一个节点SListNode* next = (*pplist)->next;//释放,改变原来的plistfree(*pplist);*pplist = next;
}

3.7 SListNode* SListFind(SListNode* plist, SLTDateType x) 的实现

这是在链表中查找值为 x 的第一个节点的,返回这个节点的指针。这个是配合 指定位置删除和插入的函数使用的。

这里不需要改变 plist,不用传 plist 的地址,查找方法就是遍历加判断。

//单链表的查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{assert(plist);//用cur遍历链表SListNode* cur = plist;while (cur != NULL){//查找到值为x的节点返回即可if (cur->data == x)return cur;cur = cur->next;}//找不到返回NULLreturn NULL;
}

3.8 void SListInsertAfter(SListNode* pos, SLTDateType x) 的实现

这个函数是在 pos 之后插入一个节点。为啥不在 pos 位置之前插入 x 呢,就是麻烦,pos位置之前插入的话,必须遍历找到pos位置之前的结构体,时间复杂度 O(N)。而在pos位置之后的插入,找到pos的下一个节点很轻松,直接插就行了。这也是单链表的缺点,无法向前找元素的哦!

这个 pos 的结构体指针就是通过 SListNode* SListFind(SListNode* plist, SLTDateType x)  的返回值来的哦!

//在pos位置之后插入 x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{assert(pos);//申请节点SListNode* newNode = BuySListNode(x);//找到pos的下一个节点SListNode* next = pos->next;//连接节点pos->next = newNode;newNode->next = next;
}

3.9 void SListEraseAfter(SListNode* pos) 的实现

这个函数是删除 pos 位置之后的节点。同样不删除 pos 位置 或者 pos位置之前的节点,都是因为找到 pos 位置之前的节点很不容易。单链表没办法!!!

//单链表删除pos位置之后的节点
void SListEraseAfter(SListNode* pos)
{assert(pos);assert(pos->next);//记录pos之后的下一个节点SListNode* next = pos->next->next;free(pos->next);pos->next = next;
}

3.10 void SListDestroy(SListNode* plist) 的实现

这个函数简单咯,遍历单链表,一个一个的释放节点就行,注意:一定要在找到下一个节点之后才能释放哦!

//单链表的销毁
void SListDestroy(SListNode* plist)
{//用cur遍历链表SListNode* cur = plist;while (cur != NULL){//找到下一个节点SListNode* next = cur->next;free(cur);cur = next;}
}

4. 题目练习

4.1 链表中的倒数第 K 个节点

剑指 Offer 22. 链表中倒数第k个节点 - 力扣(LeetCode)

题目详解:

http://t.csdn.cn/NcVND

4.2 环形链表 Ι && 环形链表 Ⅱ

141. 环形链表 - 力扣(LeetCode)

142. 环形链表 II - 力扣(LeetCode)

题目详解:

http://t.csdn.cn/YNwfJ

4.3 反转链表

剑指 Offer 24. 反转链表 - 力扣(LeetCode)

题目详解:

http://t.csdn.cn/kkUvk

4.4 合并两个有序链表

21. 合并两个有序链表 - 力扣(LeetCode)

题目详解:

http://t.csdn.cn/J1LBu

相关文章:

C语言数据结构(3)----无头单向非循环链表

目录 1. 链表的概念及结构 2. 链表的分类 3. 无头单向非循环链表的实现(下面称为单链表) 3.1 SListNode* BuySListNode(SLTDateType x) 的实现 3.2 void SListPrint(SListNode* plist) 的实现 3.3 void SListPushBack(SListNode** pplist, SLTDateType x) 的实现 3.4 voi…...

Android 实现菜单拖拽排序

效果图简介本文主角是ItemTouchHelper。它是RecyclerView对于item交互处理的一个「辅助类」,主要用于拖拽以及滑动处理。以接口实现的方式,达到配置简单、逻辑解耦、职责分明的效果,并且支持所有的布局方式。功能拆解功能实现4.1、实现接口自…...

通过window.open打开新的页面并修改样式添加内容

const img new Image(); img.src res; //res是图片的路径地址 const newWin window.open(, _blank); newWin.document.write(img.outerHTML); // newWin.document.body.style.background #000; newWin.document.body.style.textAlign center; newWin.document.body.oncl…...

Java中 Synchronized 的用法

《编程思想之多线程与多进程(1)——以操作系统的角度述说线程与进程》一文详细讲述了线程、进程的关系及在操作系统中的表现,这是多线程学习必须了解的基础。本文将接着讲一下Java线程同步中的一个重要的概念synchronized. synchronized是Java中的关键字&#xff0c…...

Rust语言的基本介绍

rust缘起和目标 rust的英文是锈菌,是一种真菌,这种真菌的生命力非常顽强,其 在生命周期内可以产生多达5种孢子类型,这5种生命形态还可以相互转 化。“Rust”也有“铁锈”的意思,暗合“裸金属”之意,代表了R…...

新冠小阳人症状记录

原想挺过春节后再养,发现事与愿违。生理期期间抵抗力下降,所以在生理期第二天就有些症状了。可能是生理期前一天出去采购食物染上,也可能是合租夫妻染上。anyway,记录下自己的症状与相应有效的偏方: 第一天&#xff1a…...

SQL零基础入门学习(十四)

上篇:SQL零基础入门学习(十三) SQL NULL 值 NULL 值代表遗漏的未知数据。 默认地,表的列可以存放 NULL 值。 如果表中的某个列是可选的,那么我们可以在不向该列添加值的情况下插入新记录或更新已有的记录。这意味着该…...

Excel工作表不能移动或复制?看看是不是这两个原因

Excel工作表不能移动或复制?今天来看看如何解决。 大家都知道,Excel表格分为工作簿和工作表,工作簿就是整个Excel文件;工作簿里面,也就是Excel表可以有多个工作表。 而各个工作表之间是可以相互移动或复制的&#xf…...

利用递归实现括号匹配

案例引入以下则是各个字符串经过括号处理之后的结果:12((21))(12-->12(21)1232((((2121)212(21)-->32(2121)212(21)ABDF((SA)SA)SA(SA)SA(((-->ABDF((SA)SA)SA(SA)SA算法思路:这个问题的解决方法就是将字符按顺序逐一加入到新的string容器store…...

14.线程数量怎么制定?

什么是CPU 密集型任务和耗时 IO 型任务 ? CPU 密集型任务 CPU 密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。 耗时 IO 型任务 数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗…...

C++中STL标准模板库学习记录

文章目录:1.vector1.1 遍历方式1.2 构造函数1.3 容量大小问题1.4 插入和删除1.5 存取值1.6 交换两个vectot的元素1.7 预定义存储空间2.string3. deque4. stack4.1 常用函数5. queue5.1 特点5.2 方法6. list6.1 优点6.2 缺点6.3 构造函数6.4 交换6.5 大小6.6 插入和删…...

《数据库系统概论》学习笔记——第六章 关系数据理论

教材为数据库系统概论第五版(王珊) 这一章重点在于各种范式的概念和将低级范式转为高级范式。一定要看多值依赖和4NF(因为这个概念很绕又烦,但是期中期末都考了)。最后计算题就是一定要会:算闭包&#xff0…...

Odoo | Webserivce | 5分钟学会【JSONRPC】接口开发

文章目录Odoo - JsonRPC1. Odoo内方法结构(接收端)2. POST接口请求结构(发送端)3. 实例测试Odoo - JsonRPC 1. Odoo内方法结构(接收端) # -*- coding: utf-8 -*- import odoo import logging import trac…...

搜广推 NeuralCF - 改进协同过滤+矩阵分解的思想

😄 NeuralCF:2017新加坡国立大学提出。【后文简称NCF】 😄 PNN:2016年上海交通大学提出。 文章目录 NeuralCF动机原理general NCFNCF终极版(GMF+MLP的结合)缺点优点ReferenceNeuralCF 动机 前面学了MF,可知MF在用户-物品评分矩阵的基础上做矩阵分解(用户矩阵Q和物品…...

dbever连接kerberos认证的hive

文章目录一、本地安装kerberos客户端二、本地kerberos客户端登录三、dbever连接hive一、本地安装kerberos客户端 下载地址:https://web.mit.edu/kerberos/dist/index.html 安装:下一步或者自定义安装即可 安装后会自动生成配置文件:C:\Pro…...

pom依赖产生的各种问题

文章目录问题一(org.apache.ibatis.session.Configuration)解决方法问题二(ERROR StatusLogger No log4j2)解决方法问题三(com.google.common.util.concurrent)解决方法问题四(start bean documentationPluginsBootstrapper)解决方法问题五(Unable to infer base url. )解决办法…...

RPC编程:RPC框架设计目标

一:前导知识 Http是超文本传输协议,跨平台性非常好。Http可以传输文本,更多的时候传输的是文本,我们也是可以传输二进制的,我们基于Http进行下载的时候,就是走的Http协议。 Tcp协议,处理的时候…...

RBAC 权限模型介绍

RBAC 权限: 一、关系: 这基于角色的访问控制的结构就叫RBAC结构。 二、RBAC 重要对象: 用户(Employee):角色施加的主体;用户通过拥有某个或多个角色以得到对应的权限。角色(Role&…...

西电面向对象程序设计核心考点汇总(期末真题)

文章目录前言一、往年真题与答案1.1 改错题1.2 读程题1.3 面向对象程序设计二、易错知识点2.1 构造函数2.2 静态成员变量和静态成员函数2.3 权限2.4 继承2.5 多态总结前言 主要针对西安电子科技大学《面向对象程序设计》的核心考点进行汇总,包含总共8章的核心简答。…...

判断一个用字符串表达的数字是否可以被整除

一.问题引出 当一个数字很大的时候,我们常用字符串进行表达,(超过了int和long等数据类型可以存储的最大范围),但是这个时候我们该如何判断他是否可以被另一个数整除呢? 这个时候我们不妨这样来考虑问题,每次将前边求模之后的数保存下来,然后乘以10和这一位的数字进行相加的操…...

基于算法竞赛的c++编程(28)结构体的进阶应用

结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...

VB.net复制Ntag213卡写入UID

本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

Java如何权衡是使用无序的数组还是有序的数组

在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比

在机器学习的回归分析中,损失函数的选择对模型性能具有决定性影响。均方误差(MSE)作为经典的损失函数,在处理干净数据时表现优异,但在面对包含异常值的噪声数据时,其对大误差的二次惩罚机制往往导致模型参数…...

Mysql8 忘记密码重置,以及问题解决

1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

Ubuntu系统多网卡多相机IP设置方法

目录 1、硬件情况 2、如何设置网卡和相机IP 2.1 万兆网卡连接交换机,交换机再连相机 2.1.1 网卡设置 2.1.2 相机设置 2.3 万兆网卡直连相机 1、硬件情况 2个网卡n个相机 电脑系统信息,系统版本:Ubuntu22.04.5 LTS;内核版本…...

算术操作符与类型转换:从基础到精通

目录 前言:从基础到实践——探索运算符与类型转换的奥秘 算术操作符超级详解 算术操作符:、-、*、/、% 赋值操作符:和复合赋值 单⽬操作符:、--、、- 前言:从基础到实践——探索运算符与类型转换的奥秘 在先前的文…...

EEG-fNIRS联合成像在跨频率耦合研究中的创新应用

摘要 神经影像技术对医学科学产生了深远的影响,推动了许多神经系统疾病研究的进展并改善了其诊断方法。在此背景下,基于神经血管耦合现象的多模态神经影像方法,通过融合各自优势来提供有关大脑皮层神经活动的互补信息。在这里,本研…...

二叉树-144.二叉树的前序遍历-力扣(LeetCode)

一、题目解析 对于递归方法的前序遍历十分简单,但对于一位合格的程序猿而言,需要掌握将递归转化为非递归的能力,毕竟递归调用的时候会调用大量的栈帧,存在栈溢出风险。 二、算法原理 递归调用本质是系统建立栈帧,而非…...