数据结构-链表(详解)

news2024/11/26 22:31:38

前言:内容包括:链表的分类,无头单向非循环链表的增删查改的实现,带头双向循环链表的增删查改的实现

目录

链表的分类

1. 单向或者双向

​编辑

2. 带头或者不带头 

 3. 循环或者非循环

无头单向非循环链表:

​编辑 带头双向循环链表:

 链表的实现

 1、无头+单向+非循环链表增删查改实现

单链表结构:

动态申请一个节点:

单链表打印:

单链表销毁:

单链表尾插:

单链表的头插:

单链表的尾删:

单链表头删 :

单链表查找:

在某个节点之前插入:

在某个节点之后插入:

删除pos位置的值:

删除pos之后的值:

2、带头+双向+循环链表增删查改实现

双向链表结构:

动态申请一个节点:

双向链表初始化(创建哨兵位的头结点):

双向链表销毁:

双向链表打印 :

双向链表尾插 :

双向链表头插:

双向链表尾删 :

双向链表头删 :

双向链表查找 :

双向链表在pos的前面进行插入:

双向链表删除pos位置的结点 :


链表的分类

组合起来共有8种结构:

1. 单向或者双向

单向:

双向 :

 

2. 带头或者不带头 

不带头:

带头(哨兵位的头结点):

 哨兵位的头结点不存储有效数据,站岗功能

 3. 循环或者非循环

非循环:

 循环:

虽然链表的组合有8种结构,但是最常用还是一下两种结构:

无头单向非循环链表:

一般不会单独用来存数据,更多是作为其他数据结构的子结构,如哈希桶、图的邻接表

 带头双向循环链表:

一般用来单独存数据

 链表的实现

 1、无头+单向+非循环链表增删查改实现

在main函数种,plist指针作为头指针,负责在单链表创建后,指向单链表的第一个节点

SLTNode* plist = NULL;

单链表结构:

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

动态申请一个节点:

SLTNode* BuySLTNode(SLTDataType x);

malloc一块空间并存入数据后,此函数将会返回这块空间的地址 

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");//若malloc开辟失败,打印错误信息
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

单链表打印:

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

单链表销毁:

void SLTDestroy(SLTNode** pphead);

当链表的每个节点销毁完成后,指向第一个节点的指针plist需要置空,所以传入plist指针的地址(二级指针接收),plist必须置空,否则成为野指针

void SLTDestroy(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

在释放一个节点之前,需要找到下一个节点,所以先用next指针保存下一个节点的地址,然后才能放心销毁当前节点

单链表尾插:

void SLTPushBack(SLTNode** pphead, SLTDataType x);

尾插分为两种情况:

空链表尾插 和 非空链表尾插

空链表尾插:需要改变头指针plist,使得plist指向插入的第一个节点

所以存在对plist指针进行修改的情况,故而需要传入plist的指针,用二级指针pphead来接收

非空链表尾插: 遍历找到尾节点进行链接

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);//pphead是头指针plist的地址一定不为空,用assert进行断言,若是pphead为空会报错

	SLTNode* newnode = BuySLNode(x);
	if (*pphead == NULL)//空链表尾插
	{
		*pphead = newnode;//plist需要指向第一个节点
	}
	else//非空链表尾插
	{
		SLTNode* tail = *pphead;
		while (tail->next)//找到尾节点,尾节点的next就是NULL
		{
			tail = tail->next;
		}
		tail->next = newnode;//链接
	}
}

关于assert断言:

pphead一定不能为空,所以需要断言,当pphead为空时,报错

*pphead可以为空,这表明链表为空,空链表是可以插入数据的,打个比方:

银行卡里没钱了,存钱是可以的 

单链表的头插:

void SLTPushFront(SLTNode** pphead, SLTDataType x);

头插也分两种:

空链表头插:需要对plist指针进行修改,使得它指向插入的第一个节点

非空链表头插:1 新节点链接头结点 2 plist指针指向新节点(新节点成为新的头结点)

但是这两种情况下的头插方法都是通用的,空链表头插代码可以与非空链表头插共用一份代码

原因:空链表时,plist==NULL,即*pphead==NULL

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);

	SLTNode* newnode = BuySLNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

单链表的尾删:

void SLTPopBack(SLTNode** pphead);

尾删分两种情况:

链表仅有1个节点:删除此节点后,plist指针需要置空

链表不只1个节点:找到尾节点的前一个结点,当尾结点删除后,它将成为新的尾结点,则其成员next指针需要置空 

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);//链表为空不能删

	if ((*pphead)->next == NULL)//链表仅有一个结点,它的next就是NULL
	{
		free(*pphead);
		*pphead = NULL;//plist指针置空
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next)//找到尾结点的前一个结点,它将会是新的tail,则tail->next就是原来的尾结点,原来的尾结点的next指针就是NULL
		{
			tail = tail->next;
		}
		free(tail->next);//删除尾结点
		tail->next = NULL;//新的尾结点的next要置空
	}
}

单链表头删 :

void SLTPopFront(SLTNode** pphead);

头删也分两种情况:

链表仅有1个节点:删除此节点后,plist指针要置空

 链表不止1个节点:1 保存要删除的节点地址 2 头指针plist指向新的头结点 3 删除节点

但是这两种情况也共用一份代码

原因:当链表仅有1个节点,它没有下一个节点,所以它的next指针指向了NULL,

当删除这个节点后,plist会置成NULL

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);//链表为空不能删除

	SLTNode* del = *pphead;//保存要删除的节点地址
	*pphead = (*pphead)->next;//plist指向新的头结点
	free(del);
}

单链表查找:

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

在某个节点之前插入:

1 使用单链表查找函数找到某个节点的地址pos

2 在pos之前插入

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

 使用二级指针pphead:若是pos是第一个结点,则在pos之前插入就是头插逻辑,会对plist指针进行修改,所以需要传plist的地址

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)//pos是头结点
	{
		SLTPushFront(pphead,x);//在头结点之前插入即头插
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)//找到pos节点的前一个结点prev
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLNode(x);
		prev->next = newnode;//链接
		newnode->next = pos;
	}
}

在某个节点之后插入:

void SLTInsertAfter(SLTNode* pos, SLTDataType x);

不需要传入plist,因为在pos之后插入,可以轻而易举得到pos之后所有结点的地址

传入plist,可以认为需要找到某一个结点的前一个结点才需要plist,因为通过遍历才可以找到某个结点的前一个结点

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

删除pos位置的值:

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

存在删除的pos位置是头结点的情况,需要对plist进行修改,所以需要二级指针 

无需assert(*pphead)

因为assert(pos)已经保证了pos位置的有效,也保证链表不为空,若是pos位置有效(即不为NULL),则说明链表不会为空

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)//头删
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)//找到pos的前一个结点
		{
			prev = prev->next;
		}
		prev->next = pos->next;//前一个结点的next指向pos的下一个结点
		free(pos);
	}
}

删除pos之后的值:

void SLTEraseAfter(SLTNode* pos);
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);//不能对NULL进行删除操作,若是pos是尾结点的地址,则pos->next为NULL

	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
}

2、带头+双向+循环链表增删查改实现

双向链表结构:

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
}LTNode;

动态申请一个节点:

LTNode* BuyLTNode(LTDataType x)
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

 

双向链表初始化(创建哨兵位的头结点):

LTNode* LTInit();

开辟一个结点,不存储任何有效数据,作为哨兵位的头结点

当链表为空时,这个哨兵位的头结点需要自己构成一个循环,即自己的next指向自己,自己的prev指向自己 

LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

双向链表销毁:

void LTDestroy(LTNode* phead);

释放完所有的有效节点后才能释放哨兵位的头结点 

void LTDestroy(LTNode* phead)
{
	assert(phead);//phead是指向哨兵位的头结点的指针,一定不能为空

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;//记录下一个要释放节点的地址
		free(cur);
		cur = next;
	}
	free(phead);//释放哨兵位的头结点
}

双向链表打印 :

void LTPrint(LTNode* phead);
void LTPrint(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	printf("guard<==>");
	while (cur != phead)
	{
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

双向链表尾插 :

void LTPushBack(LTNode* phead, LTDataType x);
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);
	
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;

	//或者
	//LTInsert(phead,x);
}

1 尾节点的next指向newnode

2 newnode的prev指向尾节点

3 newnode的next指向哨兵位的头结点

4 哨兵位头结点的prev指向newnode

双向链表头插:

void LTPushFront(LTNode* phead, LTDataType x);
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* first = phead->next;//哨兵位的头结点后的真正的头结点
	LTNode* newnode = BuyLTNode(x);

	phead->next = newnode;
	newnode->prev = phead;

	newnode->next = first;
	first->prev = newnode;

	//或者
	//LTInsert(phead->next, x);
}

双向链表尾删 :

双向链表判空:链表若为空,则仅有一个哨兵位的头结点,它既是头,也是尾

bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
void LTPopBack(LTNode* phead)
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));//链表为空不能删

	LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;//找到尾节点的上一个节点,它将成为新的尾节点

	free(tail);
	tailprev->next = phead;//新的尾节点要连接哨兵位的头结点
	phead->prev = tailprev;

	//或者
	//LTErase(phead->prev);
}


双向链表头删 :

void LTPopFront(LTNode* phead)
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));//链表为空不能删

	LTNode* first = phead->next;
	LTNode* second = first->next;//真正头结点的下一个节点second会成为新的真正头结点,链接哨兵位的头结点
	
	phead->next = second;
	second->prev = phead;
	free(first);

	//或者
	//LTErase(phead->next);
}


双向链表查找 :

LTNode* LTFind(LTNode* phead, LTDataType x)
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

双向链表在pos的前面进行插入:

pos是某个节点的地址,可以通过查找函数得到

void LTInsert(LTNode* pos, LTDataType x)
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = BuyLTNode(x);
	LTNode* prev = pos->prev;//找到pos的前一个结点

	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

双向链表删除pos位置的结点 :

void LTErase(LTNode* pos)
void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* posPrev = pos->prev;//找到pos的前一个结点
	LTNode* posNext = pos->next;//找到pos的后一个结点

	posPrev->next = posNext;//两个链接
	posNext->prev = posPrev;
	free(pos);
}

 

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

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

相关文章

造梦日记 Printidea 用户手册

*Hi 造梦日记* 欢迎来到造梦日记的空间&#xff5e; 造梦日记Printidea是一款基于AI算法、输入文字或图片即可生成高质量图片的工具&#xff0c;由西湖大学深度学习实验室和西湖心辰联合出品&#xff0c;超强算力&#xff0c;0.8秒出图&#xff0c;目前支持微信小程序、网页端…

边缘计算盒子的特点?边缘计算盒子适用于什么场景

边缘计算盒子&#xff08;Edge Computing Box&#xff09;是一种用于边缘计算的硬件设备&#xff0c;它通常是一个小型的计算设备&#xff0c;具备一定的计算能力和存储资源&#xff0c;并且能够连接到网络。边缘计算盒子的主要目的是在物联网&#xff08;IoT&#xff09;和分布…

续ShaderEditor、Inspector之后又一成功爆品,2周260+单!

01 前言 大家好&#xff0c;我是98K&#xff01;五一前&#xff0c;我上架 Cocos Store 的『高性能割草框架』增加 Cocos Creator 2.4.x 引擎&#xff0c;已经支持的老铁可免费更新&#xff01; 后续的更新计划是&#xff1a;完善2D游戏案例、增加3D案例、支持RVO和群聚&#…

Android WebView 长按弹出的文本选择器如何监听滑动和如何弹出完全自定义的菜单栏

在这次改版中&#xff0c;h5小伙伴与我沟通说要把长按选择改用成原生的拉选框&#xff0c;之前我也没搞过呀&#xff0c;开始研究吧。 怎么研究呀&#xff0c;当然是百度一下了。 百度了一天总结如下&#xff1a; 好多文章都是告诉你如何在系统的基础上来修改自己的文字和点…

vue2 框架运行原理剖析系列(二)之 组件挂载$mount神秘之旅!!!

一、vue组件挂载 1.1 上一篇文章中&#xff0c;介绍到组件执行 mountComponent 函数&#xff0c;本文对此展开详细的讲解。 1.2 调用改方法的位置在于entry-runtime-with-compiler.js 的Vue.prototype.$mount&#xff0c;具体代码如下&#xff1a; 其中&#xff0c; &#xff…

【图像融合】Dif-Fusion:基于扩散模型的红外/可见图像融合方法

文章目录 摘要一、前言二、相关工作1.红外线和可见光的图像融合2.扩散模型&#xff08;可见博主之前的博客&#xff09; 三、方法1.红外线和可见光图像的联合扩散2*.多通道扩散特征的融合 四、实验1.实验设置2.融合性能分析&#xff08;效果展示&#xff09;3.泛化实验 总结 摘…

通知短信 API 技术细节以及发送流程机制原理解析

引言 短信是一种简单、直接、高效的通信方式&#xff0c;被广泛应用于各个领域。在移动互联网时代&#xff0c;短信成为了客户服务、政府通知、公共服务等方面的重要工具。为了更好地利用短信这种通信方式&#xff0c;通知短信 API应运而生。短信API可以帮助企业、政府和应用程…

RK3588旗舰32T人工智能多网口边缘智能网关交换机

32T边缘智能网关发布&#xff0c;助力多行业数字化升级&#xff0c;运维降本增效&#xff0c;搭载RK3588旗舰芯 搭载瑞芯微RK3588芯片的边缘智能网关XM-RK3588&#xff0c;算力可扩展至32T&#xff0c;适用于电力能源、智慧交通、智慧城市、智慧安防、智慧医疗、工业互联网等领…

前端的加密和解密,crypto-js的应用,AES / RSA / md5

每日鸡汤&#xff1a;每个你想学习的瞬间&#xff0c;都是未来你的向自己求救 内容预警*****新手内容&#xff0c;自己学习总结用****大佬请绕道 之前看https原理&#xff0c;看到对称加密和非对称加密&#xff0c;各种加密方法&#xff0c;看得云里雾里&#xff0c;即便是总结…

报错main.py: error: unrecognized arguments: stack_size 4 1001,770,123

运行从GitHub上面下载下来的代码时&#xff0c;按照作者提供的输入命令输入后报错&#xff1a; main.py: error: unrecognized arguments: stack_size 4 1001,770,123 将报错的部分在网上百度&#xff0c;找到部分方法&#xff0c;得出理解&#xff1a;输入的命令是出错的&am…

【RS专题】怎么知道你遇到的是rs风控

本文属于技术分享、如有侵权可联系本人下架 最简单的方法就是查看cookie,在控制台输入【document.cookie】 如果出现如上图中有【xxxxxxT】或者【xxxxxxP】的,并且它的值都为英文数字和下滑线加点,那么基本可以确定这个网站用了rs反爬 什么是rs反爬,下面抄一段内容 瑞数动…

【瑞数RS专题】首层代码分析,和获取eval层代码,cookie反爬虫详解

如有侵权、联系本人下架 以下面两个网站为例 1.aHR0cDovL3d3dy5mYW5nZGkuY29tLmNuL25ld19ob3VzZS9uZXdfaG91c2VfZGV0YWlsLmh0bWw= 2.aHR0cHM6Ly93d3cubm1wYS5nb3YuY24veWFvd2VuL3lwamd5dy9pbmRleC5odG1s 首先明确一下目标,我们要先获取网页200的源代码,RS5代第一次响应为…

unittest自动化测试框架讲解以及实战

为什么要学习unittest 按照测试阶段来划分&#xff0c;可以将测试分为单元测试、集成测试、系统测试和验收测试。单元测试是指对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作&#xff0c;通常指函数或者类&#xff0c;一般是开发完成的。 单元…

软件架构:软件架构设计的三个维度

架构设计是一个非常大的话题&#xff0c;不管写几篇文章&#xff0c;接触到的始终只是冰山一角&#xff0c;更多的是实践中去体会。这篇文章主要介绍面向对象OO、面向方面AOP和面向服务SOA这三个要素在架构设计中的位置与作用。   架构设计有三个维度&#xff0c;或者说是我们…

文案改写神器软件-文案改编神器

文案改写神器软件 文案改写神器软件通常致力于通过人工智能技术将一篇已有文案进行改写和改编&#xff0c;以达到复用或优化的目的。以下是文案改写神器软件通常可以做的事情&#xff1a; 改写原文&#xff1a;文案改写神器可以通过自定义规则、语法分析和文本相似性匹配等功能…

设计模式之【组合模式】,树形结构的完美解决方案

文章目录 一、什么是组合模式1、组合模式三大角色2、组合模式应用场景3、组合模式注意事项和细节 二、透明组合模式1、学院院系案例2、透明组合模式总结 三、安全组合模式1、linux目录系统案例2、安全组合模式总结 四、源码中使用的组合模式1、HashMap2、ArrayList3、MyBatis 一…

跨平台应用开发进阶(六十一):uni-app 跨平台技术开发框架可行性调研

文章目录 一、流程类二、研发类(uni-app框架)三、心得分享(研发踩坑&uni-app踩坑)四、拓展阅读 一、流程类 IOS和安卓企业开发者账户申请流程(申请渠道、需要提供的相关证明、审核时间等)。 答&#xff1a;uni-app使用HBuliderX作为开发IDE,支持邮箱、密码方式注册&#x…

95、Image Restoration with Mean-Reverting Stochastic Differential Equations

简介 主页&#xff1a;https://github.com/Algolzw/image-restoration-sde 扩散模型终于在去噪、超分辨率等应用了。 这是一种基于随机微分方程的通用图像恢复方法&#xff0c;关键结构包括均值还原SDE&#xff0c;该SDE将高质量图像转换为具有固定高斯噪声的平均状态的降级…

怎么压缩png图片的大小?4个简单高效工具分享

怎么压缩png图片的大小&#xff1f;大家都知道jpg和png是目前电脑上最主流的两大图片文件格式&#xff0c;jpg图片的体积比较小&#xff0c;因为它属于有损压缩的图片格式&#xff0c;而png图片的体积相对就要大很多了&#xff0c;因为png属于无损压缩的图片格式。大家也同样知…

Redission实现分布式锁之源码解析

Redission实现分布式锁之源码解析 1、Redission实现分布式锁之源码解析1.1 分布式锁-redission功能介绍1.2 分布式锁-Redission快速入门1.3 分布式锁-redission可重入锁原理1.4 分布式锁-redission锁重试和WatchDog机制1.5 分布式锁-redission锁的MutiLock原理 1、Redission实现…