从头到尾的数据之旅

目录

引言

链表介绍

单向链表的接口实现

结构

创建节点

头插

尾插

头删

尾删

打印

节点查找

节点前插入

节点删除 

内存释放

总结


引言

在前面的学习中,我们深入了解了顺序表,通过其增删查改的操作,我们发现了顺序表在某些情况下的劣势。尤其是在头插头删或者中间位置的插入删除操作时,由于需要挪动数据,顺序表的效率显著降低。另外,顺序表在满容时需要进行扩容,而这一过程不仅带来一定的性能开销,而且扩容过多可能导致空间浪费,扩容过少则可能频繁触发扩容操作。为了克服这些问题,引入了链表这一数据结构。链表的灵活性使其能够更高效地处理插入和删除操作,为解决顺序表的局限性提供了一种优秀的选择。

链表介绍

链表是一种基础的数据结构,它由一系列节点组成每个节点包含数据和指向下一个节点的指针。在链表中,节点之间通过指针相互连接,而非像数组那样在内存中紧密排列。

这里以单向带头链表为例,第一个节点是哨兵位节点,它不存放有效数据,只存放一个指向链表头节点的指针,除哨兵位外,链表中的每个节点都存放一个有效数据以及指向下一个节点的指针。链表通过这些指针相互链接,形成一个动态的数据结构。需要注意的是,链表的节点空间都是在堆上申请的,因此节点之间的地址在物理空间上是不连续的。然而,我们可以将它在逻辑上视为连续的结构,通过指针的连接实现节点之间的逻辑关联。

关于链表的结构在《带头双向循环链表》这一文中有介绍,这里就不过多阐述了。本文重点介绍无头单向非循环链表(下文皆简称为单向链表)的基本接口实现。

单向链表的接口实现

结构

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>typedef int STDataType;   // 定义整型数据类型 STDataTypetypedef struct SListNode  // 定义链表节点结构 SListNode
{STDataType val;  // 节点存储的数据值struct SListNode* next;  // 指向下一个节点的指针
} SListNode;
e;

创建节点

// CreateNode: 创建一个包含给定数据的新链表节点
// val: 新节点要存储的数据值
// 返回值: 返回指向新节点的指针
SListNode* CreateNode(STDataType val)
{// 使用malloc分配新节点的内存空间SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));// 检查内存分配是否成功if (newnode == NULL){perror("malloc fail");exit(-1);  // 内存分配失败,输出错误信息并终止程序}// 初始化新节点的数据和指针成员newnode->val = val;newnode->next = NULL;return newnode;  // 返回指向新节点的指针
}

首先,通过 malloc 分配了一块内存来存储新的节点。接着,通过条件判断确保内存分配成功,如果分配失败则输出错误信息,并通过 exit 终止程序。然后,将节点的数据成员 val 设置为传入的参数 val,表示节点存储的有效数据。最后,将节点的指针成员 next 初始化为 NULL,表示该节点暂时没有下一个节点。最终,函数返回创建的新节点。这段代码的主要作用是为链表创建新的节点,并初始化节点的数据和指针。 

头插

// SListPushFront: 在链表头部插入新节点
// pphead: 指向头指针的指针,传入二级指针是因为可能需要修改头指针
// val: 新节点要存储的数据值
void SListPushFront(SListNode** pphead, STDataType val)
{// 确保头指针的有效性assert(pphead);// 创建新节点SListNode* newnode = CreateNode(val);// 将新节点的下一个节点指向当前的头节点newnode->next = *pphead;// 更新头指针,使其指向新插入的节点*pphead = newnode;
}

链表的头插非常方便,只需要让新节点的next指针指向当前的头节点,然后更新头节点即可。值得注意的是,这里传入的是二级指针,因为如果头节点原本是空的话,我们就要把头节点更新为新创建的newnode 节点,如果我们传递的是一级指针,我们只能修改头节点所指向的数据而无法修改头指针本身。通过使用二级指针,我们可以在需要时修改头指针,确保链表的头正确指向新插入的节点。

尾插

void SListPushBack(SListNode** pphead, STDataType val)
{// 断言确保头指针的有效性assert(pphead);// 创建新节点并初始化SListNode* newnode = CreateNode(val);// 如果链表为空,将头指针指向新节点if (*pphead == NULL){*pphead = newnode;}else{// 否则,遍历到链表末尾SListNode* cur = *pphead;while (cur->next){cur = cur->next;}// 将新节点链接到末尾cur->next = newnode;}
}

首先,通过断言确保头指针的有效性。然后,创建一个新节点并初始化其值。接着,判断链表是否为空,如果是,直接将头指针指向新节点;否则,遍历链表到末尾,并将新节点链接到末尾。这里也再一次证明了为什么要传二级指针,当链表为空时,它也涉及到了修改头指针。

头删

void SListPopFront(SListNode** pphead)
{// 断言确保头指针的有效性和链表非空assert(pphead);assert(*pphead);// 临时指针指向头节点的下一个节点SListNode* tmp = (*pphead)->next;// 释放原头节点的内存free(*pphead);// 更新头指针为下一个节点*pphead = tmp;
}

首先,通过两个断言确保头指针的有效性和链表非空。然后,使用临时指针 tmp 指向头结点的下一个节点。接着,释放原头结点的内存,最后,更新头指针为下一个节点。这里关键点在于释放头节点内存前,要先保存下一个节点,如果上来就释放的话,下一个节点就找不到了。

尾删

void SListPopBack(SListNode** pphead)
{// 断言确保头指针的有效性和链表非空assert(pphead);assert(*pphead);// 如果链表只有一个节点,直接释放头结点并将头指针置为NULLif ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{// 否则,遍历链表到倒数第二个节点SListNode* cur = *pphead;while (cur->next->next){cur = cur->next;}// 释放尾节点的内存并将倒数第二个节点的next置为NULLfree(cur->next);cur->next = NULL;}
}

首先,通过两个断言确保头指针的有效性和链表非空。然后,判断链表是否只有一个节点,如果是,直接释放头结点并将头指针置为NULL。否则,遍历链表到倒数第二个节点,释放尾节点的内存,并将倒数第二个节点的next置为NULL,避免野指针问题。

打印

void SListPrint(SListNode** pphead)
{// 断言确保头指针的有效性assert(pphead);// 初始化当前节点指针为头指针SListNode* cur = *pphead;// 遍历链表,打印每个节点的值while (cur){printf("%d ", cur->val);cur = cur->next;}// 打印换行,表示输出结束printf("\n");
}

首先,通过断言确保头指针的有效性。然后,初始化当前节点指针为头指针,使用循环遍历链表,打印每个节点的值。这里可以只传一级指针,因为不会涉及头指针的修改,为了统一我都传了二级指针。

节点查找

SListNode* SListFind(SListNode** pphead, STDataType val)
{// 断言确保头指针的有效性assert(pphead);// 初始化当前节点指针为头指针SListNode* cur = *pphead;// 遍历链表,查找节点值等于给定值的节点while (cur){if (cur->val == val)return cur;cur = cur->next;}// 如果未找到匹配节点,返回NULLreturn NULL;
}

首先,通过断言确保头指针的有效性。然后,初始化当前节点指针为头指针,使用循环遍历链表,查找节点值等于给定值的节点。如果找到匹配节点,则返回指向该节点的指针;如果未找到匹配节点,返回NULL。这个函数用来配合接下来的两个函数,进行具体节点位置的插入或删除。

节点前插入

void SListInsert(SListNode** pphead, SListNode* pos, STDataType val)
{// 断言确保头指针和目标位置指针的有效性assert(pphead);assert(*pphead);assert(pos);// 初始化当前节点指针为头指针SListNode* cur = *pphead;// 如果目标位置是头结点if (cur == pos){// 创建新节点,将新节点插入到头结点之前SListNode* newnode = CreateNode(val);newnode->next = cur;*pphead = newnode;}else{// 否则,遍历链表找到目标位置之前的节点while (cur && cur->next != pos){cur = cur->next;}// 如果找到目标位置之前的节点,插入新节点if (cur){SListNode* newnode = CreateNode(val);newnode->next = pos;cur->next = newnode;}}
}

首先,通过断言确保头指针和目标位置指针的有效性。然后,初始化当前节点指针为头指针。如果目标位置是头结点,创建新节点并将其插入到头结点之前,其实就是头插;否则,遍历链表找到目标位置之前的节点,然后插入新节点。这里也可以不断言pos和头节点,但是这样没意义,因为如果pos为空直接尾插就好了嘛,没必要来调用这个函数。

节点删除 

void SListErase(SListNode** pphead, SListNode* pos)
{// 断言确保头指针和目标位置指针的有效性assert(pphead);assert(*pphead);assert(pos);// 初始化当前节点指针为头指针SListNode* cur = *pphead;// 如果目标位置是头结点if (cur == pos){// 更新头指针为下一个节点,释放原头结点的内存*pphead = (*pphead)->next;free(pos);}else{// 否则,遍历链表找到目标位置之前的节点while (cur && cur->next != pos){cur = cur->next;}// 如果找到目标位置之前的节点,删除目标节点,释放内存if (cur){cur->next = pos->next;free(pos);}}
}

首先,通过断言确保头指针和目标位置指针的有效性。然后,初始化当前节点指针为头指针。如果目标位置是头结点,更新头指针为下一个节点,释放原头结点的内存,这里也可以直接调用前面的头删函数;否则,遍历链表找到目标位置之前的节点,然后删除目标节点,释放内存。 

内存释放

void SListDestroy(SListNode** pphead)
{// 断言确保头指针的有效性assert(pphead);// 初始化当前节点指针为头指针SListNode* cur = *pphead;// 遍历链表,释放所有节点的内存while (cur){// 保存下一个节点的指针,以便后续访问SListNode* tmp = cur->next;// 释放当前节点的内存free(cur);// 将当前节点指针更新为下一个节点cur = tmp;}// 将头指针置为NULL,表示链表已销毁*pphead = NULL;
}

首先,通过断言确保头指针的有效性。然后,初始化当前节点指针为头指针。通过循环遍历链表,释放每个节点的内存。在释放每个节点之前,保存下一个节点的指针以便后续访问。最后不要忘了将头指针置为NULL,防止出现野指针问题,在释放内存后,手动将指针置空是一个好习惯哦。

总结

在本博客中,我们深入探讨了单向链表这一数据结构的核心概念和实现。从链表的基本介绍开始,我们详细介绍了单向链表的接口实现,包括节点的创建、头插法、尾插法、头删法、尾删法、打印链表、节点查找、节点前插入、节点删除以及内存释放这些关键操作。通过这些接口实现,读者能够全面了解单向链表的结构和各种基本操作的实现方式。总的来说,本博客为初学者提供了一个全面而易懂的单向链表学习指南,使其能够在实际应用中更加灵活地操作和理解这一重要的数据结构。想要更好地掌握链式结构的话,还需要读者自己实现一下这些接口,还可以通过刷一些链表的题目来加深对链式结构的理解,链式结构在往后的学习中还有非常大的用处。

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

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

相关文章

如何批量给文件名加相同字段?

如何批量给文件名加相同字段&#xff1f;文件名批量修改是一项蕴藏了非常多知识的电脑操作技能&#xff0c;批量修改文件名称不仅仅是修改名称这么简单&#xff0c;有时候我们还可能需要批量王文件名称中添加一些相同的文字字段&#xff0c;批量操作注重的是高效率&#xff0c;…

《Vue.js设计与实现》—Vue3响应系统的原理

一、响应式数据与副作用函数 1. 副作用函数 1-1 指令材料 在JavaScript中&#xff0c;副作用函数是指在执行过程中对外部环境产生可观察的变化或影响的函数。这种函数通常会修改全局变量、修改传入的参数、执行I/O操作&#xff08;如读写文件或发送网络请求&#xff09;、修…

论文阅读《Learning Adaptive Dense Event Stereo from the Image Domain》

论文地址&#xff1a;https://openaccess.thecvf.com/content/CVPR2023/html/Cho_Learning_Adaptive_Dense_Event_Stereo_From_the_Image_Domain_CVPR_2023_paper.html 概述 事件相机在低光照条件下可以稳定工作&#xff0c;然而&#xff0c;基于事件相机的立体方法在域迁移时性…

【教程】开始使用ipaguard进行代码加固混淆

开始使用ipaguard 前言 iOS加固保护是直接针对ios ipa二进制文件的保护技术&#xff0c;可以对iOS APP中的可执行文件进行深度混淆、加密。使用任何工具都无法逆向、破解还原源文件。对APP进行完整性保护&#xff0c;防止应用程序中的代码及资源文件被恶意篡改。Ipa Guard通过…

springMVC 学习总结(三) 拦截器及统一异常处理

一.拦截器 1.拦截器与过滤器的区别 拦截器 Interceptor 和 过滤器 Filter类似&#xff0c;主要用于拦截用户请求并作出一定处理操作&#xff0c; 但两则也有不同之处&#xff0c;如过滤器只在Servlet前后起作用&#xff0c;是Servlet规范定义的&#xff0c;是Servlt容器才支…

NGINX高性能服务器与关键概念解析

目录 1 NGINX简介2 NGINX的特性3 正向代理4 反向代理5 负载均衡6 动静分离7 高可用8 结语 1 NGINX简介 NGINX&#xff08;“engine x”&#xff09;在网络服务器和代理服务器领域备受推崇。作为一款高性能的 HTTP 和反向代理服务器&#xff0c;它以轻量级、高并发处理能力以及…

2-1基础算法-枚举/模拟

文章目录 1.枚举2.模拟 1.枚举 [例1] 特别数的和 评测系统 #include <iostream> using namespace std; bool pa(int x) {while (x) {if (x % 10 2 || x % 10 1 || x % 10 0 || x % 10 9) {return true;}else {x x / 10;}}return false; } int main() {int sum0;i…

【pytest】单元测试文件的写法

前言 可怜的宾馆&#xff0c;可怜得像被12月的冷雨淋湿的一条三只腿的黑狗。——《舞舞舞》 \;\\\;\\\; 目录 前言test_1或s_test格式非测试文件pytest.fixture()装饰器pytestselenium test_1或s_test格式 要么 test_前缀 在前&#xff0c;要么 _test后缀 在后&#xff01; …

低压无功补偿在分布式光伏现场中的应用

摘要&#xff1a;分布式光伏电站由于建设时间短、技术成熟、收益明显而发展迅速&#xff0c;但光伏并网引起用户功率因数异常的问题也逐渐凸显。针对分布式光伏电站接入配电网后功率因数降低的问题&#xff0c;本文分析了低压无功补偿装置补偿失效的原因&#xff0c;并提出了一…

代码随想录算法训练营第46天| 139.单词拆分 多重背包

JAVA代码编写 139.单词拆分 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。 **注意&#xff1a;**不要求字典中出现的单词全部都使用&#xff0c;并且字典中的单词可以重复使用。 示例 1&#xff1a; 输入: s &…

Java - JVM内存模型及GC(垃圾回收)机制

JVM内存模型 JVM堆内存划分&#xff08;JDK1.8以前&#xff09; JVM堆内存划分&#xff08;JDK1.8之后&#xff09; 主要变化在于&#xff1a; java8没有了永久代&#xff08;虚拟内存&#xff09;&#xff0c;替换为了元空间&#xff08;本地内存&#xff09;。常量池&#…

数据库中常用的锁

目录 1、数据库中常用的锁类型 2、常见的数据库 3、以MySQL为例 3.1 MySQL的事务 3.2 MySQL事务的四大特性 1. 原子性&#xff08;Atomicity&#xff09; 2. 一致性&#xff08;Consistency&#xff09; 3. 隔离性&#xff08;Isolation&#xff09; ⭐mysql中的事务隔…

temu上传产品的素材哪里找

在为Temu&#xff08;拼多多跨境电商平台&#xff09;上传产品时&#xff0c;您需要准备一些高质量的素材&#xff0c;包括图片和视频。这些素材对于吸引用户的注意力、展示产品的特点以及提高购买意愿非常重要。但是&#xff0c;很多卖家都不知道从哪里找到这些素材。本文将为…

【Deeplearning4j】小小的了解下深度学习

文章目录 1. 起因2. Deeplearning4j是什么3. 相关基本概念4. Maven依赖5. 跑起来了&#xff0c;小例子&#xff01;6. 鸢尾花分类代码 7. 波士顿房价 回归预测代码 8. 参考资料 1. 起因 其实一直对这些什么深度学习&#xff0c;神经网络很感兴趣&#xff0c;之前也尝试过可能因…

Unity-小工具-LookAt

Unity-小工具-LookAt &#x1f959;介绍 &#x1f959;介绍 &#x1f4a1;通过扩展方法调用 gameObject.LookAtTarget&#xff0c;让物体转向目标位置 &#x1f4a1;gameObject.StopLookat 停止更新 &#x1f4a1;可以在调用时传入自动停止标记&#xff0c;等转向目标位置后自…

.net 洋葱模型

洋葱架构 内层部分比外层更抽象(内层接口&#xff0c;外层实现)。外层的代码只能调用内层的代码&#xff0c;内层的代码可以通过依赖注入的形式来间接调用外层的代码 简单的例子&#xff0c;引用依赖图 demo 接口类库 EmailInfo using System; using System.Collections.…

Python安装包(模块)的八种方法,Python初学者必备知识点

文章目录 1. 使用 easy\_install2. 使用 pip install3. 使用 pipx4. 使用 setup.py5. 使用 yum6. 使用 pipenv7. 使用 poetry8. 使用 curl 管道关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Py…

轻量封装WebGPU渲染系统示例<44>- 材质组装流水线(MaterialPipeline)之灯光和阴影(源码)

目标: 数据化&#xff0c;模块化&#xff0c;自动化 备注: 从这个节点开始整体设计往系统规范的方向靠拢。之前的都算作是若干准备。所以会和之前的版本实现有些差异。 当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/feature/material/src/voxgpu/sa…

apt-get update失败

一、先验证是否有网络 rootlocalhost:~# ping www.baidu.com ping: www.baidu.com: Temporary failure in name resolution rootlocalhost:~# 说明没有网&#xff0c;参考&#xff1a;https://blog.csdn.net/qq_43445867/article/details/132384031 sudo vim /etc/resolv.con…

代码随想录二刷 | 二叉树 |404.左叶子之和

代码随想录二刷 &#xff5c; 二叉树 &#xff5c;404.左叶子之和 题目描述解题思路递归法迭代法 代码实现递归法迭代法 题目描述 404.左叶子之和 给定二叉树的根节点 root &#xff0c;返回所有左叶子之和。 示例 1&#xff1a; 输入: root [3,9,20,null,null,15,7] 输出…