单链表链表专题

1 链表的概念

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

链表的结构跟⽕⻋⻋厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只 需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。

⻋厢是独⽴存在的,且每节⻋厢都有⻋⻔。想象⼀下这样的场景,假设每节⻋厢的⻋⻔都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从⻋头⾛到⻋尾?

最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。

在链表⾥,每节“⻋厢”是什么样的呢?

与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点”节点的组成主要有两个部分:当前节点要保存的数据和保存下⼀个节点的地址(指针变量)。

图中指针变量plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希 望plist“指向”第⼆个节点时,只需要修改plist保存的内容为0x0012FFA0。 

为什么还需要指针变量来保存下⼀个节点的位置?

链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针 变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。

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

假设当前保存的节点为整型

struct SListNode
{int data; //节点数据 struct SListNode* next; //指针变量⽤保存下⼀个节点的地址 
};

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

当我们想要从第⼀个节点⾛到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下⼀个 节点的钥匙)就可以了。

给定的链表结构中,如何实现节点从头到尾的打印?

 先打印第一个节点中的数据,即pcur结构体的data成员,pcur结构体的next指针指向下一个节点(pcur结构体的next指针保存着下一个节点的地址)。我们把这个地址重新赋值给pcur,此时pcur就指向了第二个节点。循环这一过程,直到pcur指向空指针时跳出循环。这样就实现了链表的遍历。

2单链表的实现

2.1开辟新节点

ListNode* NewListNote(DataType x)
{ListNode* newlistnode = (ListNode*)malloc(sizeof(ListNode));if (newlistnode == NULL){perror("malloc error");exit(1);}newlistnode->data = x;newlistnode->next = NULL;return newlistnode;
}

我们对链表进行插入操作时,需要像内存申请一个节点大小的空间,这里我们用到了malloc函数。

申请新空间并且为结构体的成员赋值。

2.2尾插

在链表的尾部插入一个新节点

void LiseTailAdd(ListNode** pphead, DataType x)
{assert(pphead);ListNode* newlistnode = NewListNote(x);if (*pphead == NULL){*pphead = newlistnode;}else{ListNode* ptail = *pphead;while ((ptail)->next != NULL){ptail = (ptail)->next;}(ptail)->next = newlistnode;}
}

如果链表中一个节点都没有,就直接插入一个新节点,新节点作为链表的“头节点”。

如果链表中原来就有节点,那我们应该先找到尾节点,然后在尾节点后边插入新节点。

这里为什么传入二级指针呢?

因为链表的头节点的地址可能会因为新节点的插入而发生改变(原链表中一个节点都没有视情况下)。想要头节点的地址发生改变,就必须传二级指针。即函数的传址调用。

2.3头插

在链表的头部插入一个新节点

void LiseHeadAdd(ListNode** pphead, DataType x)
{assert(pphead);ListNode* newlistnode = NewListNote(x);newlistnode->next = *pphead;*pphead = newlistnode;
}

建一个新节点,再让新节点的next指针指向头节点,然后让这个新节点作为链表的“头节点”。

2.4头删

删除链表的“头节点”

void LiseHeadDel(ListNode** pphead)
{assert(pphead);ListNode* tmp = (*pphead)->next;free(*pphead);*pphead = tmp;
}

直接释放头节点,让它的下一个节点作为链表的新的头节点,但是直接free头节点后,我们就找不到了它的下一个节点,所以要在释放之前用一个变量tmp把他下一个节点的地址保存下来。再让*pphead指向tmp。

2.5尾删

删除链表的尾节点

void LiseTailDel(ListNode** pphead)
{assert(pphead);ListNode* per = *pphead;ListNode* ptail = *pphead;while (ptail->next){per = ptail;ptail = ptail->next;}per->next = NULL;//free(ptail);//ptail = NULL;
}

要删除尾节点,我们应该把尾节点释放掉,并且让为节点的前一个节点的next指针指向空。所以我们要先找到尾节点和尾节点的前一个节点。找尾节点的方法和尾插找尾的方法类似。找到尾后释放即可。

2.6查找数据

ListNode* FindNote(ListNode* phead, DataType x)
{while (phead){if (phead->data == x){printf("找到了\n");return phead;}phead = phead->next;}printf("没找到");
}

遍历链表,查找某个数据是否存在,如果存在就返回对应节点的地址。不存在的话就打印没找到。

2.7在指定位置之前插入

void PopNoteFrontAdd(ListNode** pphead, ListNode* pop, DataType x)
{assert(pphead);if (*pphead == pop){LiseHeadAdd(pphead, x);}else{ListNode* per = *pphead;while (per->next != pop){per = per->next;}ListNode* newlistnode = NewListNote(x);newlistnode->next = pop;per->next = newlistnode;}
}

 在指定位置之前插入,我们需要找到这个位置之前的节点,让这个节点的next指针指向新节点,然后新节点的next指针在指向pop。

所以要遍历链表,通过头节点找到这个位置之前的节点。

2.8在指定位置之后插入

void PopNoteBehindAdd(ListNode* pop, DataType x)
{ListNode* newlistnode = NewListNote(x);newlistnode->next = pop->next;pop->next = newlistnode;
}

在指定位置之后插入,只需要让新节点的next指针指向pop的next节点,再让pop的next指针指向新节点。

注意这两条指令的顺序不能颠倒,因为如果先让pop的next指针先发生改变,就找无法找到原来的pop的next节点。

2.9删除pop之后位置节点

void DelPopBehindNode(ListNode* pop)
{assert(pop && pop->next);ListNode* tmp = pop->next;pop->next = tmp->next;free(tmp);tmp = NULL;
}

释放pop之后位置的节点,但释放之后无法找到这个位置的下一个节点,所以释放之前要用变量tmp把pop->next先存起来。

2.10删除pop位置的节点

void DelPopNode(ListNode* pop, ListNode** pphead)
{assert(pphead);if (pop == *pphead){LiseHeadDel(pphead);}else{ListNode* per = *pphead;while (per->next != pop){per = per->next;}per->next = pop->next;free(pop);pop = NULL;}
}

如果pop是头节点,直接调用头删函数。

如果pop不是头节点,那应该找到pop的前一个结点,并让其next指针指向pop的下一个节点,最后释放pop节点。

2.11链表销毁

void SListDesTroy(ListNode** pphead)
{assert(pphead && *pphead);while (*pphead != NULL){ListNode* next = (*pphead)->next;free(*pphead);*pphead = next;}
}

遍历链表,逐个节点释放。但是直接释放的话会找不到下一个节点,所以要有一个变量next将下一个节点的地址存起来。

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

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

相关文章

Laravel 11入门:使用ServBay打造高效开发环境

Laravel 11发布,改进了不少功能。 它引入了更加流畅的应用结构、每秒限速、健康路由等特性。 此外,Laravel还推出了第一方可扩展的WebSocket服务器Laravel Reverb,为你的应用提供强大的实时功能。 在今天的指南中,我将设置一个…

【C语言__动态内存管理__复习篇6】

目录 前言 一、动态内存管理 二、动态内存函数 2.1 malloc 2.2 free 2.3 calloc 2.4 realloc 三、动态内存常见的6个使用错误 3.1 接收malloc/calloc返回的参数后未及时检查是否为NULL 3.2 越界访问动态内存空间 3.3 对非动态开辟的内存使用free释放 3.4 使用free只释放了…

「 典型安全漏洞系列 」14.NoSQL注入漏洞详解

NoSQL注入是一个漏洞,攻击者能够干扰应用程序对NoSQL数据库进行的查询,本文我们将研究如何测试一般的NoSQL漏洞,然后重点研究如何利用MongoDB中的漏洞(MongoDB是最流行的NoSQL数据库)。 1. 什么是NoSQL注入 NoSQL注入…

[lesson33]C++中的字符串类

C中的字符串类 历史遗留问题 C语言不支持真正意义上的字符串C语言用字符数组和一组函数实现字符串操作C语言不支持自定义类型,因此无法获得字符串类型 解决方案 从C到C的进化过程引入自定义类型在C中可以通过类完成字符串类型的定义 标准库中的字符串类 C语言直…

古月·ROS2入门21讲——学习笔记(一)核心概念部分1-14讲

讲解视频地址:1.ROS和ROS2是什么_哔哩哔哩_bilibili 笔记分为上篇核心概念部分和下篇常用工具部分 下篇:古月ROS2入门21讲——学习笔记(二)常用工具部分15-21讲-CSDN博客 目录 第一讲:ROS/ROS2是什么 1. ROS的诞生…

Java实现单点登录(SSO)详解:从理论到实践

✨✨谢谢大家捧场,祝屏幕前的小伙伴们每天都有好运相伴左右,一定要天天开心哦!✨✨ 🎈🎈作者主页: 喔的嘛呀🎈🎈 ✨✨ 帅哥美女们,我们共同加油!一起进步&am…

[docker] 核心知识 - 容器/镜像的管理和操作

[docker] 核心知识 - 容器/镜像的管理和操作 想要查看完整的指令,可以通过 docker --help 列举所有的指令,这里会提到一些比较常用的核心指令 查看容器的状态 这个应该是最常用的指令,语法为 docker ps, ps 为 process status …

【详解算法流程+程序】DBSCAN基于密度的聚类算法+源码-用K-means和DBSCAN算法对银行数据进行聚类并完成用户画像数据分析课设源码资料包

DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一个比较有代表性的基于密度的聚类算法。 与划分和层次聚类方法不同,它将簇定义为密度相连的点的最大集合,能够把具有足够高密度的区域划分为簇, 并可在噪声的空间数据…

使用DockerCompose配置基于哨兵模式的redis主从架构集群

文章目录 一、注意事项(坑点!!!)二、配置Redis主从架构集群第一步:创建目录文件结构第二步:编写DockerCompose配置文件第三步:编写redis.conf第四步:启动redis主从集群 三…

python 重载内置函数吗

python中是不支持函数重载的,但在python3中提供了这么一个装饰器functools.singledispatch,它叫做单分派泛函数,可以通过它来完成python中函数的重载,让同一个函数支持不同的函数类型,它提供的目的也正是为了解决函数重…

【Linux C | 多线程编程】线程同步 | 互斥量(互斥锁)介绍和使用

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀 🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C、数据结构、音视频🍭 ⏰发布时间⏰: 本文未经允许…

【verilog】 reg与寄存器的关系

一、前言 在Verilog中经常用reg定义具有数据寄存功能的单元,但在verilog的使用中,并不代表其一定就是寄存单元,reg还能进行组合逻辑描述,并且在一些场景下,只能使用reg来申明变量。 二、reg型变量生成组合逻辑 在Ve…