详细介绍:【数据结构初阶】单链表

news/2025/11/27 20:43:29/文章来源:https://www.cnblogs.com/ljbguanli/p/19279428

详细介绍:【数据结构初阶】单链表

目录

1. 顺序表问题与思考

2. 单链表

2.1 概念与结构

2.1.1 结点

2.1.2 链表的打印

3. 实现单链表

3.1 第1步:尾插

3.2 第2步:头插

3.3 第3步:尾删

3.4 第4步:头删

3.5 第5步:查找

3.6 第6步:在指定位置之前插入数据

3.7 第7步:在指定位置之后插入数据

3.8 第8步:在pos位置删除数据

3.9 第9步:在pos位置之后删除数据

3.10 第10步:销毁单链表

3.11 全部代码

4. 单链表算法题

4.1 移除链表元素

4.2 反转链表

4.3 链表的中间结点

4.4 合并两个有序的链表

4.5 链表分割

4.6 链表的回文结构

4.7 相交链表

4.8 环形链表


1. 顺序表问题与思考

  • 中间/头部的插入删除,时间复杂度为O(N)
  • 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗
  • 增容一般是呈2倍的增长,势必会造成空间的浪费。例如当前容量为100,放满后增容到200,我们再继续插入5个数据,后面没有数据的插入了,那么就会浪费95个数据空间了。

对于这些问题我们该如何解决呢? 就是使用链表。我们想把时间复杂度降为O(1),想让空间刚好够,或者浪费少量空间等等。

2. 单链表

2.1 概念与结构

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

结合着概念以及第一节的思考,我们可以想到现在我们大部分人出远门的方式——火车。淡季的时候我们就会减少车厢的数量,旺季的时候就会增加车厢的数量,尤其是候补的人数够几个车厢就会加几个车厢,这样分配位置合理,且不会造成浪费。

注:该图片由豆包ai生成。

就如上图,火车有4节车厢,不够了我们还可以添加车厢。那么放在和它类似的链表中又是怎么样的呢?

2.1.1 结点

与顺序表不同的是,我们把上面图中的每一块申请的独立空间叫做结点。结点的组成主要有两个部分:当前结点要保存的数据和保存下一个节点的地址(指针变量)。

由上图不难看出,每个结点的空间都是独立的也就是在堆中:

其中箭头是肯定没有的。

所以我们就可以知道链表的性质:

  1. 在逻辑结构上是连续的,在物理结构上不一定是连续的。
  2. 结点一般是在堆上申请的。
  3. 从堆上申请的空间可能是连续的,也可能是不连续的。

2.1.2 链表的打印

我们有了上面知识的铺垫,就可以创建一个链表,然后将它打印出来。

// 打印链表
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur != NULL){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
void Test01()
{// 创建一个链表SLTNode* node1= (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node2= (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node3= (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node4= (SLTNode*)malloc(sizeof(SLTNode));// 链表的初始化node1->data = 1;node2->data = 2;node3->data = 3;node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;// 打印链表SLTNode* plist = node1;SLTPrint(plist);
}

这里为什么会循环打印出来呢?我们把下一个结点的地址给了pcur,然后他就会继续识别pcur,直到pcur指向空指针也就是链表的末尾。

3. 实现单链表

我们了解了如何使用,现在我们来实现单链表。

3.1 第1步:尾插

从逻辑上我们写出了尾插的代码,但是会不会有问题呢?这段代码看起来是没有问题的。我们要养成良好的习惯,在写完一段代码后进行测试,我们来测试一下:

这和我们预期的结果不一样,为什么会这样呢?在一些小伙伴看来,plist也是地址,应该是没问题的,这里说明一下原因:因为plist中存储的是一个地址,但是在形参中是不会被改变的,所以在本质上来讲是传值调用,只不过是这个值是一个指针,所以我们这里就得使用二级指针。现在我们来看一下修改后的代码:

在进行测试的时候发现,在申请结点后发现并未让newnode初始化,我们这里给加上。

// SList.h
#include 
#include 
#include 
// 创建一个链表结构
typedef int SLTDataType;
typedef struct SListNode
{int data;struct SListNode* next; // 指向下一个节点的指针
}SLTNode;
// 打印链表
void SLTPrint(SLTNode* phead);
// 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
// SList.c
#include "SList.h"
// 打印链表
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur != NULL){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
// 申请新结点
SLTNode* SLTBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (malloc == NULL){perror("malloc erro");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}
// 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);// 在插入结点之前,我们需要申请一个新的结点// 申请新结点SLTNode* newnode = SLTBuyNode(x);// phead 为空的时候if (*pphead == NULL){*pphead = newnode;}else{SLTNode* ptail = *pphead;while (ptail->next != NULL){ptail = ptail->next;}ptail->next = newnode;}
}
// test.c
void test02()
{// 创建空链表SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPushBack(&plist, 5);// 打印链表SLTPrint(plist);
}

现在程序可以正常运行,也就是尾插代码我们已经写完。

3.2 第2步:头插

从画图中,我们知道了,让newnode的next指针指向第一个结点的地址,然后在让newnode成为第一个结点。现在我们来实现:

头插非常简单,这已经写完了,其中由于担心pphead为空指针,我们来断言一下,保险。

3.3 第3步:尾删

我们仍然通过画图明确逻辑,然后实现代码。

大体的思路我们是有了,但是还是会有缺陷,我们先来实现这段逻辑。

我们前几次的删除都是没有问题的,但是问题就是这最后一次,只有一个结点的时候,prev和ptail指向同一个结点,第一次循环就会跳出循环,所以应该做特殊处理。

这时候尾删我们已经实现。

3.4 第4步:头删

3.5 第5步:查找

// 查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{while (phead->next){SLTNode* pcur = phead;if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}

这段代码的实现非常简单,遍历链表即可。

3.6 第6步:在指定位置之前插入数据

画图:

有了这个逻辑我们来实现一下:

这里是直接把特殊情况包含了,也就是pos属于头结点。

3.7 第7步:在指定位置之后插入数据

这里逻辑清晰后代码是很好写的。所以我们直接写代码,然后做测试。

3.8 第8步:在pos位置删除数据

我们来写代码:

3.9 第9步:在pos位置之后删除数据

其中,我们删除数据后也不能使pos->next指向空,所以我们需要断言。

3.10 第10步:销毁单链表

怎么销毁,一个一个销毁。

3.11 全部代码

#pragma once
// SList.h
#include 
#include 
#include 
// 创建一个链表结构
typedef int SLTDataType;
typedef struct SListNode
{int data;struct SListNode* next; // 指向下一个节点的指针
}SLTNode;
// 打印链表
void SLTPrint(SLTNode* phead);
// 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
// 头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
// 尾删
void SLTPopBack(SLTNode** pphead);
// 头删
void SLTPopFront(SLTNode** pphead);
// 查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
// 在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
// 在pos位置删除数据
void SLTErase(SLTNode** pphead, SLTNode* pos);
// 删除pos位置之后的数据
void SLTErasrAfter(SLTNode** pphead, SLTNode* pos);
// 销毁链表
void SLTDestroy(SLTNode** pphead);
#define _CRT_SECURE_NO_WARNINGS 1
// SList.c
#include "SList.h"
// 打印链表
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur != NULL){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
// 申请新结点
SLTNode* SLTBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (malloc == NULL){perror("malloc erro");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}
// 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{// 在插入结点之前,我们需要申请一个新的结点// 申请新结点SLTNode* newnode = SLTBuyNode(x);assert(pphead);// phead 为空的时候if (*pphead == NULL){*pphead = newnode;}else{SLTNode* ptail = *pphead;while (ptail->next != NULL){ptail = ptail->next;}ptail->next = newnode;}
}
// 头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);// 申请新结点SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}
// 尾删
void SLTPopBack(SLTNode** pphead)
{// 首先我们来断言一下,防止传过来的是空链表assert(pphead && *pphead );// 只有一个结点的情况if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* prev = NULL;SLTNode* ptail = *pphead;while (ptail->next){prev = ptail;ptail = ptail->next;}prev->next = NULL;free(ptail);// 养成一个好习惯,释放完空间,置为空ptail = NULL;}
}
// 头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}
// 查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}
// 在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && pos);// 申请一个新结点SLTNode* newnode = SLTBuyNode(x);if (pos == *pphead){// 头插SLTPushFront(pphead, x);}// 寻找pos位置的前一个结点SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}// 找到后prev->next = newnode;newnode->next = pos;
}
// 在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);// 申请新结点SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}
// 在pos位置删除数据
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && pos);if (pos == *pphead){//头删SLTPopFront(pphead);}else{SLTNode* prev = *pphead;// 找到pos位置前的结点while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}
// 删除pos位置之后的数据
void SLTErasrAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}
// 销毁链表
void SLTDestroy(SLTNode** pphead)
{assert(pphead);SLTNode* pcur = *pphead;while (pcur != NULL){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
#define _CRT_SECURE_NO_WARNINGS 1
// test.c
#include "SList.h"
void Test01()
{// 创建一个链表SLTNode* node1= (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node2= (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node3= (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node4= (SLTNode*)malloc(sizeof(SLTNode));// 链表的初始化node1->data = 1;node2->data = 2;node3->data = 3;node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;// 打印链表SLTNode* plist = node1;SLTPrint(plist);
}
void test02()
{//// 创建空链表// 尾插SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);// 打印链表SLTPrint(plist);//// 头插//SLTNode* plist = NULL;//SLTPushFront(&plist, 1);//SLTPrint(plist);//SLTPushFront(&plist, 2);//SLTPrint(plist);//SLTPushFront(&plist, 3);//SLTPrint(plist);//SLTPushFront(&plist, 4);//SLTPrint(plist);//// 尾删//SLTPopBack(&plist);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);//// 头删//SLTPopFront(&plist);//SLTPrint(plist);//SLTPopFront(&plist);//SLTPrint(plist);//SLTPopFront(&plist);//SLTPrint(plist);//SLTPopFront(&plist);//SLTPrint(plist);// 查找SLTNode* pos = SLTFind(plist, 1);/*if (pos){printf("zhaodaole");}else{printf("weizhaodao");}*///// 在指定位置之前插入数据/*SLTInsert(&plist, pos, 100);SLTPrint(plist);SLTInsert(&plist, pos, 100);SLTPrint(plist);SLTInsert(&plist, pos, 100);SLTPrint(plist);*//*SLTInsertAfter(pos, 100);SLTPrint(plist);*/SLTErase(&plist, pos);SLTPrint(plist);
}
int main()
{// Test01();test02();return 0;
}

最后链表的销毁没有做测试。

4. 单链表算法题

4.1 移除链表元素

点标题就可以进入题目网址:

我们读完题,并给出了自己的思路,现在我们在服务端完成算法代码:

我们在题库中写完代码并测试且通过了。有了思路写代码就是非常快的。所以我们在以后遇到算法题的时候,一定要先整理思路,然后开始写代码。这样准确率和速度的提升都是非常巨大的。

4.2 反转链表

同上我们进行读题然后寻找思路:

我们来实现思路2的代码:

这么一看是不是特别简单。这道题就到这里了。

4.3 链表的中间结点

我们直接画图。

代码我们实现思路2。

很显然是通过的。但是我们会有一个疑问,如果把while的循环条件位置互换会发生什么事?

报错,原因是空指针的解引用。

4.4 合并两个有序的链表

这里有点着急,没有移动newTail,大家应该可以理解。

我们有了思路后,就可以实现代码了。

我们按照思路写完了代码,但是是不通过的。当然我们在写代码的过程肯定不是一帆风顺的,有错我们应该兴奋起来,这样可以完善我们的短板,如果每次写的代码都是一遍过我们会觉得非常没意思,有错我们将它修改正确这才是满满的成就感。

我们现在来看一下报的错误:

第57行发生空指针的解引用。

也就是俩者都为空的时候,会发生解引用错误,我们应该特殊处理一下,在开头我们判断list1和list2是否为空。

在开头增加了list1和list2的判别自测示例就可以通过。

同时终端也是通过的。

所以这段代码就算是写完了。但是我们会感觉有点冗余,我们现在来优化。

对比可以看出,代码量缩短了,其中使用到了哨兵位,就是不储存数据,只是占一个位置。

这道题的介绍就到这里结束了。

4.5 链表分割

还是老样子我们先画图找思路

有了思路直接写代码。

我们也是成功提交了,主要要注意大链表的尾要指向空。

4.6 链表的回文结构

这样写代码就简单多了。当然,如果题目中没有最后一句话,我们就老老实实的使用第一种思路。

4.7 相交链表

这样我们就实现了判断两链表的相交节点。

4.8 环形链表

我们在平台上实现思路。

代码就是这么简单,我们现在来看看while循环的条件。

通过我们判别不是环链表,我们就可以得出while循环条件。

接下来我们来证明为什么快慢指针在换链表中一定会相遇,同时证明fast走三步可不可以。

其实我们陷入了思维误区,我们把这个问题抽象为物理问题,就可以看出,无论速度快慢 (走多少步),它俩在环中必然会相遇只是时间问题。

链表题还有很多很多,我们这里就不在进行举例,我们该有的思维,在这些例子中都有涉及。

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

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

相关文章

第五十篇

今天是11月27号,上了体育和数据结构

每日随笔

今天背了第四单元的英语单词,看了一会《代码大全2》

2025年日语自学软件推荐:最适合零基础与进阶者的优质口碑选择

2025年日语自学软件推荐:最适合零基础与进阶者的优质口碑选择学习一门新语言时,合适的自学工具往往能让我们少走弯路。对于选择自学的日语爱好者来说,如何在众多日语自学软件中找到真正适合自己的那一款日语自学软件…

ABC386 VP总结

比赛链接 ResultE 题没开 LL 爽挂 3 发,F 题咋是压哨过的 Solution F - Operate K 令 \(dp_{i,j}\) 为 \(S\) 的前 \(i\) 位和 \(T\) 的前 \(j\) 为的最小编辑距离,转移是显然的。因为 \(dp_{i,j}\) 只会从 \(dp_{i,…

探究Spring Boot框架中访问不存在的接口时触发对error路径的访问

先说结论 默认情况下在Spring Boot框架中访问不存在的接口时会触发对"/error"路径的访问,这是由Spring Boot框架的默认错误处理机制导致的,核心是ErrorMvcAutoConfiguration自动配置类在起作用。 追根溯源…

tarjan 强连通分量、缩点、点双、割点、割边(桥)

有向图 强连通分量、缩点 取 cmin(low[u], dfn[v]) 时 v 一定要在栈里。 弹栈时要将 u 也弹出。 int dfn[N], low[N], dfnp, st[N], sp, vis[N], bl[N], blp; void tarjan(int u) {vis[st[++sp] = u] = 1;dfn[u] = low…

2025最新智慧停车与门禁系统解决方案推荐——骏通智能,专注出入口控制与智能化管理,车牌识别、道闸管理、门禁解决方案、通道闸、停车场服务、人脸门禁一站式解决

随着智慧城市建设的加速推进,智慧停车与智能门禁系统已成为现代建筑、社区及商业场所的标配设施。在2025年的出入口控制与智能化管理领域,骏通智能凭借多年技术沉淀与创新实力,为各类场景提供高效、安全、智能的解决…

我踩坑后总结:企业微信客服API接入客服系统,90%的人都搞错了!

vx:llike620 gofly.v1kf.com 最近在配置企业微信客服时,我在域名备案这个问题上踩了不少坑,结果发现大多数人的理解都存在误区。今天就把我的实战经验分享给大家,帮你少走弯路! 两个后台,两种不同的规则 首先必须…

香橙派上进行MQTT数据存储客户端开发(一)基本环境配置

香橙派上进行MQTT数据存储客户端开发(一)基本环境配置基本信息 云服务器配置:EMQX Cloud 类型为 Serverless 主机:Orangepi5max 16G (ARM64 架构) + 32G tf卡 系统:Orangepi5max_1.0.0_ubuntu_jammy_desktop_xf…

GEO 优化价格大比拼,哪家最便宜?三大高性价比机构推荐

GEO 优化已成为企业拓展本地市场、精准触达目标客户的核心手段。无论是线下门店引流、区域品牌推广,还是本地服务曝光,优质的 GEO 优化都能让企业在搜索引擎、地图应用等平台占据更靠前的位置,从而获取更多潜在客户…

2025年AI学习机哪个品牌好?热门品牌功能与效果全解析

2025年AI学习机哪个品牌好?热门品牌功能与效果全解析一、推荐背景与评价体系 最新教育硬件调研数据显示,2025年家长在选择AI学习机时最关注的三大痛点为:“学习内容是否精准适配”、“AI技术是否成熟可靠”、“学习…

2025年知名的长租公寓有哪些:权威榜单与精选解析

2025年知名的长租公寓有哪些:权威榜单与精选解析在城市青年的居住选择谱系中,焦虑正从零散的个体困扰演化为具有普遍性的时代命题。面对海量租房选项与参差的服务现实,许多人不得不在品牌公寓、中介平台与个人房源间…

编程中的枚举法与数学上的穷举法有何区别?

枚举法和穷举法在核心思想上都强调“逐一尝试所有可能性”,但在编程和数学中的侧重点、应用场景及实现方式存在显著差异。以下是具体对比及示例说明: 1. 核心区别维度 枚举法(编程) 穷举法(数学)目的 通过遍历所…

如百钱百鸡问题,枚举法和穷举法有何不同

百钱百鸡问题是一个经典的数学谜题,要求用100文钱买100只鸡,其中公鸡5文钱一只,母鸡3文钱一只,小鸡1文钱三只。求解公鸡、母鸡、小鸡的数量组合。枚举法和穷举法在解决该问题时,核心思路都是遍历所有可能的组合,…

2025年长租公寓排名:最新专业榜单与推荐

2025年长租公寓排名:最新专业榜单与推荐在城市青年的居住版图中,租房早已不只是找个落脚处,而是一次关乎生活品质与精神归属的系统选择。面对品牌公寓、中介平台与个人房源的多重岔路,许多人陷入“租房迷茫”——耗…

从零开始建网站在线客服系统:域名+服务器,到底怎么选才不踩坑?

🌐 从零开始建站:域名+服务器,到底怎么选才不踩坑? 你是不是也听过这种说法:“建网站嘛,域名和服务器肯定少不了!”——没错,这确实是铁律。但作为一个刚折腾完个人博客的过来人,我想告诉你:真正关键的,不…

根本魔法语言数组 (一) (C语言)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025年租房品牌排名:TOP10权威揭秘与必读

2025年租房品牌排名:TOP10权威揭秘与必读在租房市场中,租客们常常陷入“租房迷茫”与“体验落差”的困境。他们在“品牌公寓”“中介租房”“个人房源”等众多选择中徘徊,租房过程繁琐,不仅耗费大量时间和精力,还…

Spring Cloud工程中使用Nacos配置中心的2种方式

先说结论 使用Nacos作为配置中心时,因工程配置文件名称的不同,配置Nacos参数的方式也有所不同。 如下示例使用的框架及服务版本信息为:Spring Boot:2.6.13 Spring Cloud:2021.0.5 Spring Cloud Alibaba:2021.0.5…

《程序员修炼之道:从小工到专家》阅读笔记5

1.纯文本的威力:坚持使用纯文本(如JSON, YAML,源代码)来存储知识。 好处:可读性、可移植性、易于被各种工具(如grep, awk, sed)处理。它是知识的“持久存储”最佳格式。 2.Shell的力量:不要轻视命令行。它是你的…