从头到尾的数据之旅

news2024/11/22 14:46:49

目录

引言

链表介绍

单向链表的接口实现

结构

创建节点

头插

尾插

头删

尾删

打印

节点查找

节点前插入

节点删除 

内存释放

总结


引言

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

链表介绍

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

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

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

单向链表的接口实现

结构

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int STDataType;   // 定义整型数据类型 STDataType

typedef 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);

    // 如果链表只有一个节点,直接释放头结点并将头指针置为NULL
    if ((*pphead)->next == NULL)
    {
        free(*pphead);
        *pphead = NULL;
    }
    else
    {
        // 否则,遍历链表到倒数第二个节点
        SListNode* cur = *pphead;
        while (cur->next->next)
        {
            cur = cur->next;
        }

        // 释放尾节点的内存并将倒数第二个节点的next置为NULL
        free(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;
    }

    // 如果未找到匹配节点,返回NULL
    return 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.coloradmin.cn/o/1304769.html

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

相关文章

移液器吸头材质选择——PFA吸头在半导体化工行业的应用

PFA吸头是一种高性能移液器配件&#xff0c;这种材料具有优异的耐化学品、耐热和电绝缘性能&#xff0c;使得PFA吸头在应用中表现出色。那么它有哪些特点呢&#xff1f; 首先&#xff0c;PFA吸头具有卓越的耐化学腐蚀性能。无论是酸性溶液、碱性溶液还是有机溶剂&#xff0c;P…

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

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

版本控制:让你的代码有迹可循

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

人工智能在智慧工地中的应用场景

随着料技的不断发展&#xff0c;人工智能在各个领域都有着广泛的应用。智慧工地作为人工智能在建筑行业的应用领域之一&#xff0c;通过引入人工智能技术&#xff0c;可以提高工地的管理效率、降低事故发生概率、提升工人的工作效率&#xff0c;从而实现智能化、自动化的工地管…

IPEmotion 2023 R3 现支持新款数据记录仪IPE833

新发布的IPEmotion 2023 R3增加了多种新功能&#xff0c;其中最重要的新功能包括支持最新的数据记录仪IPE833和用于XCP测量的报文转信号功能。此外&#xff0c;它还增加了一项用于提高记录仪安全性的新功能。 — 创新一览 — ■ 支持新款数据记录仪IPE833 • 四路CAN FD接口&…

24款CBR600RR复活,CBR1000R电控下放,有望引进?

最近本田在欧洲市场亮相了停产已经6年的24款本田CBR600RR&#xff0c;传说中的F5复活了&#xff01;24款CBR采用了全新的外观设计&#xff0c;可以看到前面也加上了流行的定风翼&#xff0c;不过设计是娇小一点的&#xff0c;另外本田的CBR600RR也是唯一在售的采用尾排设计的仿…

18、Web攻防——ASP安全MDB下载植入IIS短文件名写权限解析

文章目录 一、MDB默认下载1.1 搭建IIS网站1.2 搭建网站会出现的一些问题1.2 攻击思路 二、ASP后门植入连接三、IIS短文件名探针——安全漏洞四、IIS6.0文件解析漏洞五、IIS6.0任意文件上传漏洞 一、MDB默认下载 web攻防常见开发语言&#xff1a;ASP、ASPX、PHP、JAVA、Python、…

基于ssm四六级报名与成绩查询系统论文

摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对四六级报名信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性…

一体化超声波气象站科普解说

随着科技的不断发展&#xff0c;气象监测设备也在逐步升级。一体化超声波气象站作为新型气象监测设备&#xff0c;以其优势和预报能力&#xff0c;成为了气象监测领域的新宠。 一、一体化超声波气象站的特点 WX-CSQX12 一体化超声波气象站是一种集成了多种气象监测设备的新型…

蓝桥杯--数的拆分

蓝桥杯省赛 数的拆分&#xff0c;应该是一道数论的题目 连接&#xff1a;数的拆分 对有所有的样例&#xff0c;应该用long long 来表示。 n的范围是1e18次方&#xff0c;暴力绝对是行不通的&#xff0c;不能用暴力来解决。 这是一道数学的题目&#xff0c;需要对题目进行分…

链表TOP难度——排序链表

https://leetcode.cn/problems/sort-list/?envTypestudy-plan-v2&envIdtop-interview-150 采用分治思想解决这题&#xff0c;每次合并长度为1\2\4…的链表&#xff0c;合并思想和合并有序链表一致&#xff0c;单独写成一个函数即可。 合并思路如下&#xff1a; while(分治…

赛900定妆照亮相3个色,配Stylema卡钳,风琴排气,后减震有黑科技?

钱江的赛900工信部早就给大家报道过&#xff0c;使用的是奥古斯塔的921发动机&#xff0c;之前有说叫赛1000&#xff0c;又说叫赛921&#xff0c;现在名字终于是定了&#xff0c;叫赛900RR&#xff0c;那么目前钱江赛系列的产品线就清晰了&#xff1a;赛1000、赛900、赛800、赛…

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

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

GoEasy使用手册

GoEasy官网 登录 - GoEasy 即时通讯聊天案例 GoEasy - GoEasy (gitee.com) 注意事项 接口使用人数上限为15&#xff0c;超出之后会请求超时返回408状态码&#xff0c;可以新建一个应用用来更换common Key 创建应用 ​ 添加应用名称&#xff0c;其余默认&#xff0c;点击…

python使用vtk与mayavi三维可视化绘图

VTK&#xff08;Visualization Toolkit&#xff09;是3D计算机图形学、图像处理和可视化的强大工具。它可以通过Python绑定使用&#xff0c;适合于科学数据的复杂可视化。Mayavi 依赖于 VTK (Visualization Toolkit)&#xff0c;一个用于 3D 计算机图形、图像处理和可视化的强大…

第三届iEnglish全国ETP大赛16强落位 诠释教育游戏价值

10日,与北方骤降的温度形成鲜明对比,以“玩转英语,用iEnglish”为主题的国内首个教育游戏活动第三届iEnglish全国ETP(English Through Pictures)大赛总决赛小组赛热火朝天的进行。随着“云帆沧海队”搭上末班车,本届活动16强全部产生,接下来的三个周末他们将向年度总冠军发起最…

nuitka Unknown property box-shadow,transition,transform

nuitka 打包后&#xff0c;控制台的错误解决方法 nuitka --standalone --show-memory --show-progress --nofollow-imports --follow-import-toneed --output-dirout --windows-icon-from-ico./static/test.ico mainUI2.py 由于Qt样式表不是CSS&#xff0c;QSS基于CSS2.1&…

百度地图通过DrawingManager.js改造绘制电子围栏,圆形、矩形、多边形、行政区域。( 方式2)

故事&#xff1a;在新项目中&#xff0c;还是需要绘制围栏&#xff0c;由于前面使用的vue-BMap 官方方式进行围栏绘制&#xff0c;虽说比较灵活&#xff0c;但代码量比较大&#xff0c;而且手工敲的代码量太大&#xff0c;因此进行第二中电子围栏的绘制探索。 注意&#xff1a…

traj_dist 笔记 源代码解析(python部分)

1distance.py 1.1 METRIC_DIC 不同实现方法对应的函数路径 1.2 sspd 功能&#xff1a; 计算轨迹 traj_1 和 traj_2 之间的对称化段路径距离。 参数&#xff1a; traj_1&#xff1a;一个二维 numpy 数组&#xff0c;代表第一个轨迹。traj_2&#xff1a;一个二维 numpy 数组…

??题-结构体两个人成绩比较输出分数高的【有问题,可是不知道在那里】

#include<stdio.h>struct stu{long int num;char name[10];double score;}a[2];int main(){ int i;for(i0;i<2;i)scanf("%ld,%s,%lf",&a[i].num,&a[i].name,&a[i].score);if(a[0].score>a[1].score)printf("分数高的学号和姓名是&…