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

单链表讲解

一.链表的概念以及结构

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

链表的结构与火车是类似的,一节一节的,数据就像乘客一样在车厢中一样。

与顺序表不同的是,链表里的每节"车厢"都是独立申请的空间,我们将这样的空间称为节点或者结点

 既然链表是一节一节的结点构成的,那么这样的结点是怎么连接的呢?

链表中的节点一般是通过结构体来实现的,结构体中的存储着数据和下一个节点的地址,这样我们就可以访问下一个节点中的数据了。

节点:

typedef struct SListNode
{SLTDataType val;struct SListNode* next;
}SLTNode;

为了使用方便我们可以对其重命名。

二.单链表的实现

typedef int SLTDataType;
typedef struct SListNode
{SLTDataType 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);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);

首先我们来思考如何打印数据呢?

void SLTPrint(SLTNode* phead);

这个参数是指向第一个头节点的指针,为什么要使用指针呢?

通过指针我们可以访问这个结构体中的元素,如果我们不使用指针,形参是实参的一份临时拷贝,如果数据过大,这样会很浪费空间(我们这里存储的数据是整形,为了泛用性,我们还是要使用一级指针),直接使用会降低程序性能。

phead->data就可以访问到数据了,那么下一个节点的数据呢?

这时候就会用到我们在结构体中存储的指针了,这个指针指向下一个结构体,所以再通过下一个结构体我们就可以访问下一个结构体的数据,同理,下下个结构体的指针同理。

void SLTPrint(SLTNode* phead) {SLTNode* pcur = phead;while (pcur) {printf("%d->>",pcur->data);pcur = pcur->next;}printf("NULL\n");
};

通常习惯上,我们是不会改变直接传过来的参数的,所以我们要使用的话,就再使用一个变量来进行接收。

这里pcur不为NULL时,代表我们的指针并没有走到结构体的末尾,我们就可以以它作为限制条件,每经过一个节点,就打印一次数据,直到遇到空指针停止。 

然后就是要完成指定位置之前插入数据了。

假设有一个链表1->>2->>3->>5->>NULL.

我们要在数据5的前面插入4.可是在插入之前,我们要找到数据五的位置才能插入。

如何查找元素呢?

SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

第一个参数是指向第一个节点的指针,第二个是要寻找的数据,虽然这里可能会存在相同的数字,但是这只是我们练习的场景,假设我们要存储人的身份信息,这是不可能存在相同的人的,所以我们这里假设数据不相同。

最后返回这个节点的地址。

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

和打印数据相同。我们只需要遍历整个数组即可,如果找到就返回这个节点的地址,否则就返回空。

接着我们再来完成指定位置插入数据。

即使我们找打这个节点的地址,我们要在它之前插入数据,也就是再创建一个节点,然后把这个节点的next指针指向找的节点,这样我们就在指定节点前面插入了数据。

但是我们新建的这个节点是没有被节点指向的,所以我们还要让前一个节点指向新的节点。

这个SLTBuyNode是创建新节点函数,我们在插入数据时,会频繁用到这个代码,如果重复的写,就太浪费时间了,所以我们单独封装一个函数来完成这个功能。

SLTNode* SLTBuyNode(SLTDataType x) {SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL) {perror("malloc");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}
		SLTNode* pcur = *pphead;while (pcur->next != pos) {pcur = pcur->next;}pcur->next = SLTBuyNode(x);pcur->next->next = pos;

可是这样写就完了吗?

如果我们要在链表的第一个节点之前插入数据,我们会发现这个代码循环是根本没有进入的,并且会访问NULL地址,这是非法的。

所以为了防止这样的问题,我们还需要额外区分头插的情况。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {assert(pphead);assert(*pphead);if (*pphead == pos) {*pphead = SLTBuyNode(x);(*pphead)->next = pos;}else {SLTNode* pcur = *pphead;while (pcur->next != pos) {pcur = pcur->next;}pcur->next = SLTBuyNode(x);pcur->next->next = pos;}
};

 然后我们来思考,为什么这里要使用二级指针,我们使用指针的目的是为了在函数中改变指针所指向的变量的值,这里就是传值和传址的区别了,我们在头插的过程中指向头节点的指针,是会改变的,如果我们传一级指针,是改变不了这个指针的,所以我们要使用二级指针。

然后就是删除数据函数

void SLTErase(SLTNode** pphead, SLTNode* pos);

这里与插入函数一样,是需要考虑头删的。

void SLTErase(SLTNode** pphead, SLTNode* pos) {assert(pphead);assert(*pphead);SLTNode* pcur = *pphead;if (pos == *pphead) {*pphead = pos->next;free(pos);}else {while (pcur->next != pos) {pcur = pcur->next;}pcur->next = pos->next;free(pos);pos = NULL;}
};

删除指定节点,只需要我们将前一个节点指向下一个节点就行了,然后再释放指定节点即可 。

而头删就是释放头节点,然后将头指针指向头节点的下一个节点既可以。

为了方便,头插头删位插尾删,我们都使用前面两个函数来进行完成,这样更加方便。

void SLTPushBack(SLTNode** pphead, SLTDataType x) {assert(pphead);if (*pphead == NULL) {*pphead = SLTBuyNode(x);}else {SLTInsert(pphead,NULL,x);}
}

在尾插时,如果链表为空,我们就直接创建一个新的节点即可,否则我们就只需要在NULL前面插入一个节点即可,因为最后一个节点的next指针指向的是空。

void SLTPushFront(SLTNode** pphead, SLTDataType x) {assert(pphead);if (*pphead == NULL) {*pphead = SLTBuyNode(x);}else {SLTInsert(pphead, *pphead, x);}
};

头插同理,如果链表为空直接创建新的链表即可,否则就是在头节点之前插入即可,而且头节点直接作为参数给我们使用了,所以我们就不需要再查找了. 

void SLTPopBack(SLTNode** pphead) {assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur->next != NULL) {pcur = pcur->next;}SLTErase(pphead,pcur);
};

尾删就需要我们遍历链表,找到最后一个链表的指针 ,这样再删除即可。链表为空就不能删了,所以我们要断言一下。

void SLTPopFront(SLTNode** pphead) {assert(pphead && *pphead);SLTErase(pphead,*pphead);
};

头删更简单,不需要遍历,直接调用删除函数即可。

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {assert(pos);SLTNode* next = SLTBuyNode(x);next->next = pos->next;pos->next = next;
};
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos) {assert(pos);SLTNode* next = pos->next;pos->next = next->next;free(next);next = NULL;
};

 这里是同理,要插入就直接创建新链表然后插入即可,删除也是同理,这里是需要判断pos是否为空的,因为如果没有找到find函数就会返回空。

删除链表

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

就需要遍历整个链表然后依次释放,最后为了防止野指针,我们还需要将*pphead置为NULL。 

三.链表 

链表的种类有很多种。

任意两两组合就有2 * 2 * 2 = 8种组合

这个带头和不带头后面双链表再讲解。

我们通常所说的单链表是不带头单向不循环链表。

单向就是通过一个只能访问下一个节点不能访问上一个节点,否则就是双向。

循环就是链表围成一个圈,链表的最后一个节点next指向头节点了,而不是NULL.

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

 

http://www.lryc.cn/news/339194.html

相关文章:

  • DFS算法系列 回溯
  • Linux C应用编程:MQTT物联网
  • 企业常用Linux文件命令相关知识+小案例
  • Istio介绍
  • 代码随想录算法训练营第四十七天|leetcode115、392题
  • 将Ubuntu18.04默认的python3.6升级到python3.8
  • Python和Java哪个更适合后端开发?
  • Python+pytest接口自动化之cookie绕过登录(保持登录状态)
  • 什么数据集成(Data Integration):如何将业务数据集成到云平台?
  • 国外EDM邮件群发多少钱?哪个软件好?
  • C语言入门算法——回文数
  • OceanBase—操作实践
  • 智慧用电安全管理系统
  • Rust语言入门第二篇-Cargo教程
  • 测试用例的编写方式
  • HarmonyOS实战开发-状态管理、通过使用页面级的状态变量 和应用级的状态变量 来实现应用的状态管理。
  • 【Java开发指南 | 第二篇】标识符、Java关键字及注释
  • 3D可视化技术:研发基地的科技新篇章
  • 蓝旭前端05:JavaScript进阶
  • 【docker-compose】安装及配置
  • 【第十五届】蓝桥杯省赛C++b组
  • thinkphp6 Driver [Think] not supported.
  • 爱自然生命力专项基金:“爱·启航”残障家庭教育援助项目帮扶上万残障家庭
  • 【ubuntu】如何追加path
  • 用html写一个有趣的鬼魂动画
  • 【C++软件调试技术】C++软件开发维护过程中典型调试问题的解答与总结
  • Pygame经典游戏:贪吃蛇
  • 推荐一个免费使用Claude 3, GPT4和Gemini 1.5 Pro的网站
  • An Investigation of Geographic Mapping Techniques for Internet Hosts(2001年)第二部分
  • 解锁生成式 AI 的力量:a16z 提供的 16 个企业指南