详解单链表

news/2024/7/9 4:22:08

 

 💕十载寒窗无人问,一举成名天下知💕

作者:Mylvzi 

 文章主要内容:程序环境和预处理 

引言:

  我们之前已经学习过顺序表,顺序表是一种线性的存储结构,它在内存中是连续存放的;我们不难发现,顺序表在管理数据时存在一些问题,如进行插入数据时需要挪动大量数据,异地扩容导致内存使用率低,存在大量内存碎片等等,那有没有一种存储方式可以实现“随用随开“的内存使用方式呢?存在,就是我们今天要学的-->链表

链表的基本知识:

  链表是一种管理内存的数据结构,和顺序表一样,都是一种线性表;

单链表(Single List Table)的表示:

(指针域中只有一个地址,所以叫做单链表)

链表与顺序表的区别:

1.链表和顺序表最大的差距在于空间开辟的方式:

链表是有多少就开辟多少(精打细算)

顺序表是直接给你一大片空间,让你使用,不够用了,再给你一大片空间(任性父母)

2.顺序表的空间在内存中是连续的,而链表的空间是一个一个独立的小空间,不连续

单链表的创建: 

 前提准备:

//创建结构体存储单链表的基本信息(数据域 + 指针域):

typedef int SLDataType;
//定义结点
typedef struct SListNode
{
	SLDataType data;//数据域
	struct SListNode* next;//指针域-->存放下一节点的地址
}SLTNode;//SLT-->single list table单链表

单链表的管理:

打印单链表:

逻辑:从头指针(phead)访问下一个结点,打印每个结点的数据域,再把下一个结点的地址给cur,一直到NULL;

//打印链表
void SLTPrint(SLTNode* phead);

//打印链表逻辑
void SLTPrint(SLTNode* phead)
{
	//assert(phead);err
	//断言不合理,因为phead可以是NULL,代表链表中无数据
	//断不断言,要看的传的数据是否合理

	SLTNode* cur = phead;//重新赋头指针
	//while(cur != NULL)
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;

		//++cur err 结点的存储在内存中不是连续的
	}

	printf("NULL");
}

创建新结点: 

逻辑:为新结点动态开辟内存空间,然后再对数据域指针域进行赋值

//创建一个新结点
SLTNode* BuySListNode(SLDataType x);

//创建一个新结点
SLTNode* BuySListNode(SLDataType x)//要存储的数据为x
{
	//为整个结点动态开辟空间
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(-1);
	}

	//初始化
	newnode->data = x;
	newnode->next = NULL;
}

尾插:

 

//尾插-->结点的本质是一种结构体指针,改变结构体指针需要传递结构体指针的地址(二级指针)
void SLTPushBack(SLTNode** pphead, SLDataType x);

//尾插-->结点的本质是一种结构体指针,改变结构体指针需要传递结构体指针的地址(二级指针)
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
    assert(pphead);//pphead是plist的地址,永远不可能为空,所以要检查;
	//assert(*pphead);err  空链表可以尾插

	//创建一个新结点作尾插用
	SLTNode* newnode = BuySListNode(x);

	//进行尾插-->找到尾结点,使其next指向newnode
	if (*pphead == NULL)
	{
		//如果*phead本事就是NULL,则不存在NULL->next,直接将newnode赋给phead即可
		newnode = *pphead;
	}
	else
	{
		//寻找到尾结点
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

 头插:

//头插
void SLTPushFront(SLTNode** pphead, SLDataType x);

//头插
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
    assert(pphead);
	SLTNode* newnode = BuySListNode(x);

	//头插-->注意顺序
	newnode->next = *pphead;
	*pphead = newnode;
	
	//err
	//*pphead = newnode;
	//newnode->next = *pphead;
}

注意到:头插和尾插的参数都是二级指针,原因在于你改变的是结点,结点本身就是一个结构体指针,改变结构体指针就要传递结构体指针的地址,即二级指针;在顺序表中,我们改变的是一个一个结构体 ,所以传递的是结构体指针

由于形参是实参的临时拷贝,出了函数作用域后会被自动销毁,要改变值,就要传递值的地址!

头插,尾插都要检查pphead,但不用检查*pphead,因为NULL也能插入数据

尾删:

//尾删
void SLTPopBack(SLTNode** pphead);

//尾删
void SLTPopBack(SLTNode** pphead)
{
    assert(pphead);//pphead为空,不合理
	//1.*pphead == NULL  如果是NULL,属于非法删除
	assert(*pphead);

	//2.只有一个节点
	if ((*pphead)->next == NULL)//注意:由于存在优先级问题,*pphead需要添加括号
	{
		free(*pphead);
		*pphead = NULL;
	}
	else//不止一个结点
	{
		//第一种写法
		SLTNode* tail = *pphead;

		//while (tail->next->next != NULL)
		while (tail->next->next)//在倒数第二个结点停止(因为你需要删除最后一个结点)
		{
			tail = tail->next;
		}

		free(tail->next);
		tail->next = NULL;
	}

	第二种写法:双指针法
	//	SLTNode* tailPrev = NULL;//tail结点的上一个结点
	//	SLTNode* tail = *pphead;

	//	while (tail->next)
	//	{
	//		tailPrev = tail;
	//		tail = tail->next;
	//	}
	//	free(tail);
	//	tail = NULL;
	//	//tailPrev->next = NULL;
}

 注意尾删有三种情况

 头删:

//头删
void SLTPopFront(SLTNode** pphead);

//头删
void SLTPopFront(SLTNode** pphead)
{
    assert(pphead);
	//1.空
	assert(*pphead);

	//2.非空
	SLTNode* newhead = (*pphead)->next;//使第二个结点作为头结点
	free(*pphead);
	*pphead = newhead;//这里不是置空,而是置换新结点为头结点
}

寻找结点:

根据输入的值,找到对应的结点

//寻找数据为x的结点
SLTNode* SLTFind(SLTNode* phead, SLDataType x);

SLTNode* SLTFind(SLTNode* phead, SLDataType x)
{
	assert(phead);
	SLTNode* cur = phead;//遍历尽量多定义一个变量

	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	//出循环,遍历到NULL
	return NULL;
}

在pos之前插入x:

逻辑:找到pos之前的prev结点,改变指针域

 

// 在pos之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);

// 在pos之前插入x-->单链表前插效率不高
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
	assert(*pphead);

	//pos是*pphead-->头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);//头插函数的第一个参数是plist的地址,是二级指针
	}
	else
	{
		//找到pos之前的结点prev
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySListNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

在pos之后插入x: 

逻辑:找到pos,改变指针域(去观察什么数据发生了改变)

 

// 在pos以后插入x
void SLTInsertAfter(SLTNode* pos, SLDataType x);

// 在pos以后插入x
void SLTInsertAfter(SLTNode* pos, SLDataType x)
{
	assert(pos);//防止传错

	//插入逻辑
	SLTNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

删除pos位置的结点:

逻辑:找到prev,改变指针域,free(pos)

// 删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos);

// 删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
    assert(pphead);
	assert(*pphead);
	assert(pos);

	//头指针-->头删
	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}
	else
	{
		//找到prev
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

删除pos的后一个位置的结点: 

 逻辑:找到posNext,改变指针域,free(posNext);

// 删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos);

// 删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);//检查pos是否是尾节点

	SLTNode* posNext = pos->next;
	pos->next = posNext->next;

	free(posNext);
	posNext = NULL;
}

总结: 

  单链表是一种链式的线性表,是一种常见的数据结构,但其对数据的管理效率并不高(只有头插,头删的效率还可以),但常常出题考察,在leetcode上非常常见,此外,单链表还经常作为更复杂数据结构的子结构出现,所以还是要掌握好单链表的相关知识,以及他的创建管理!希望这篇文章对你有用,谢谢观看!


http://lihuaxi.xjx100.cn/news/1439515.html

相关文章

pgsql checkpoint机制(1)

检查点触发时机 检查点间隔时间由checkpoint_timeout设置pg_xlog中wall段文件总大小超过参数max_WAL_size的值postgresql服务器在smart或fast模式下关闭手动checkpoint 为什么需要检查点? 定期保持修改过的数据块作为实例恢复时起始位置(问题&#xf…

Fast SAM与YOLOV8检测模型一起使用实现实例分割以及指定物体分割

Fast SAM与YOLOV8检测模型一起使用 部分源代码在结尾处可获取 晓理紫 1 使用场景 实例分割数据集的获取要比检测数据的获取更加困难,在已有检测模型不想从新标注分割数据进行训练但是又想获取相关物体的mask信息以便从像素级别对物体进行操作,这时就可以…

uniapp 小兔鲜儿 - 首页模块(1)

目录 自定义导航栏 静态结构 安全区域​ 通用轮播组件 静态结构 自动导入全局组件 全局组件类型声明 .d.ts文件 注册组件 vue/runtime-core 首页 – 轮播图指示点 首页 – 获取轮播图数据 首页 – 轮播图数据类型并渲染 首页 – 轮播图总结 首页分类 首页 – 前…

系统架构设计专业技能 · 网络规划与设计(三)【系统架构设计师】

系列文章目录 系统架构设计专业技能 网络规划与设计(三)【系统架构设计师】 系统架构设计专业技能 系统安全分析与设计(四)【系统架构设计师】 系统架构设计高级技能 软件架构设计(一)【系统架构设计师…

keil下载程序具体过程4:flash下载算法

引言 本篇文章将介绍flash算法文件,阐述从jlink如何下载镜像文件写入到内部的falsh。 一、XIP 在谈flash下载算法文件时,先说明XIP是什么。 芯片的启动方式有很多种:可以从RAM中启动、内部的flash、外部的flash等等(还有从sd卡、…

常见的路由协议之RIP协议与OSPF协议

目录 RIP OSPF 洪泛和广播的区别 路由协议是用于在网络中确定最佳路径的一组规则。它们主要用于在路由器之间交换路由信息,以便找到从源到目标的最佳路径。 常见的路由协议: RIP (Routing Information Protocol):RIP 是一种基于距离向量算…

java字符串String类的常用方法

java字符串String类的常用方法 字符串的创建: (1)定义字符串直接赋值,在字符串池中开辟空间() String str1“Hello”;//在字符串池中写入字符串"hello" String str2“Hello”;//直接引用字符串池中的"Hello" System.out.println(s…

【Unity实战系列】Unity的下载安装以及汉化教程

君兮_的个人主页 即使走的再远,也勿忘启程时的初心 C/C 游戏开发 Hello,米娜桑们,这里是君兮_,怎么说呢,其实这才是我以后真正想写想做的东西,虽然才刚开始,但好歹,我总算是启程了。今天要分享…