【数据结构】单链表(超全)

news2024/9/22 8:22:24

在这里插入图片描述

目录

  • 一、什么是链表?
    • 1.1 定义
    • 1.2 链表的分类
  • 二、无头单向非循环链表
    • 2.1 结构
    • 2.2 如何遍历链表数据
    • 2.3 尾插
    • 2.4 创建新节点
    • 2.5 头插
    • 2.6 尾删
    • 2.7 头删
    • 2.8 单链表查找
    • 2.9 在pos位置之前插入
    • 2.10 删除pos位置数据
    • 2.11 在pos位置的后面插入
    • 2.12 删除pos位置后面的数据

前言:
 上一次我们分享了线性表中的一种结构顺序表,它存在着一些其缺点,比如:在中间位置或者头部进行元素插入或者删除的时候时间复杂度是 O ( N ) O(N) O(N)效率比较低,并且顺序表在扩容的时候也存在时间和空间上的消耗,由于我们每次都是按照二倍来扩的,那就很有可能会出现扩大了用不完导致空间浪费的现象。这些问题该如何解决呢?那就需要用到今天分享给大家的另一种线性结构 链表

一、什么是链表?

1.1 定义

 链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
 简单来理解就是,链表中的每一个数据元素都是独立存储的,当需要存储一个数据元素的时候就去向内存空间申请一块内存用来存储当前数据,每一个数据元素又通过地址串联在一起,因此对于链表这个结构来说,不仅需要存储当前的数据元素,还需要存储下一个元素的地址,这样才能把数据元素串联在一起,通常我们把待存储的数据元素和下一个数据的地址合起来叫做链表中的一个节点,这个节点由两部分数据组成,因此可以定义一个结构体来创建节点。
图示:
在这里插入图片描述

1.2 链表的分类

 链表会根据是否带头结点、是否是双向链表、是否是循环链表进行分类组合。因此链表一共有八种具体的实现形式。
 其中头节点是在链表的第一个节点之前附设一个类型相同的节点,我们把这个节点就称之为头节点,当然头节点和普通的节点在形式上并没有什么不同,所以头节点也分数据区域指针区域,头结点的数据区域建议什么东西也不存储,有些地方会在头结点的顺序区域存储链表的长度,其实这是欠妥的,因为链表的长度(也就是节点个数)一定是一个int型的整型变量,只有当链表存储的全是整型数据的时候,节点的数据域是整型,此时在头结点的数据域存储链表长度是可以的,但是当链表存储的是字符型或者其他用户自定义类型的时候,那节点的数据域就不是整型了,此时再在头结点的数据域存储链表长度显然是不行的。所以,为了使我们写出来的链表更加具有普适性,这里不建议大家在头结点的数据域存储链表长度。头结点的指针域存储的则是链表中第一个节点的地址。关于链表的优点这里先留一个悬念,相信随着学习的深入小伙伴们自然就能体会到头结点的优势。
在这里插入图片描述

 这里大家需要注意区分头节点头指针,他俩是两回事。

  • 头节点是一个节点,本质上是一个结构体变量,区分数据域和指针域
  • 头指针是一个指针,本质上是一个结构体类型的指针变量,不区分数据域和指针域,它仅存储链表中第一个节点的地址。

 双向链表,即一个节点中有两个指针域,一个存放当前节点前一个节点的地址,另一个存放当前节点后一个节点的地址。随着学习的深入我们就会发现双向链表的优势,这里就不过多赘述。
在这里插入图片描述

 循环链表,即链表的最后一个节点的指针域存的是第一个节点的地址,这样一来整个链表就形成了一个环达到了循环的效果。
在这里插入图片描述

 至此,链表的分类就给大家介绍完了,虽然根据链表的不同分类标准可以组合出八种不同结构的链表,但我们只要掌握了无头单向非循环带头双向循环这两种结构,剩下的六种便能轻松上手。所以下面我将详细的介绍一下这两种结构。

二、无头单向非循环链表

在这里插入图片描述

2.1 结构

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;//数据域
	struct SListNode* next;//指针域
}SLTNode

 无头单向非循环链表的结构就只需要数据域一个指针域。数据域的类型是待存储数据的类型,这里为了使链表更具有普适性,这里使用了typedef对类型重命名。指针域要存放的是下一个节点的地址,因此它的类型是一个结构体类型的指针,需要注意:结构体的类型是struct SListNode一定要写全,不能漏写,也不能用SLTNode来声明指针,因为在声明指针的时候还没有对结构体类型进行重命名。为了方便起见,对结构体类型进行重命名。

链表与顺序表在结构上的区别:
在这里插入图片描述
 从图中可以看出,链表的每一个数据是直接存储在一个结构体变量中,多个结构体变量共同组成一个链表,而顺序表则是在一个结构体变量的基础上,通过它的成员arr指向动态申请的空间,顺序表中的数据并没有直接存储在结构体变量中,而是存储在动态申请的开空间里,一个顺序表只对应一个结构体变量。结构上的差异导致pList == NULLps == NULL所表达的含义是不同的,pList == NULL表示当前链表是一个空链表,当然空链表也是一个链表,只不过里面没有数据罢了,而ps == NULL则说明这个顺序表根本就不存在,这里注意:ps == NULL不表示一个空顺序表,ps->size == 0才表示一个空顺序表(前提是ps非空)。

2.2 如何遍历链表数据

 在知道了什么是链表以及链表的结构之后,我们需要把链表利用起来,即完成对链表的增、删、查、改等工作。在进行这些工作之前,我们首先得知道如何去遍历链表访问链表中的每一个数据,然后才能去完成前面的工作。遍历过程如下代码所示:

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

 首先形参接收到的是链表中第一个节点的地址,要遍历链表当然要从第一个结点开始嘛,这一点相信大家很容易理解,接下来就是遍历了,遍历是通过一个指针去维护的,可以这么说对链表的所有操作都是用一个指针去维护的,通过改变指针的指向以达到访问不同节点的目的,因为链表是由多个节点共同组成的,说白了也就是多个结构体变量共同组成的,如果是值传递,那这个链表有多少个节点,我们就得在函数中再创建多少个节点来接收,这在造成空间浪费的同时也大大增加了我们处理问题的复杂度。在明确了用指针去遍历链表之后,接下来就该让这个指针动起来。cur = cur->next;这条语句就实现了让指针动起来,把cur指向的节点的指针域中存的地址赋值给cur自己,而cur指向的节点的指针域存放的就是下一个节点的地址。直到cur == NULL的时候说明已经遍历完了整个链表。需要注意遍历结束的条件不能是:cur->next != NULL,因为cur->next == NULL说明cur指向最后一个节点,并没有遍历结束。下面给出遍历过程的物理示意图和逻辑示意图。
物理结构示意图:
在这里插入图片描述
逻辑结构示意图:
在这里插入图片描述

2.3 尾插

 尾插的第一步当然是先创建一个节点(也就是结构体类型的变量)来存储数据,注意尾插我们是通过函数来实现的,因此在创建新节点的时候不能用SLTNode newnode来创建节点,因为这样创建的节点是一个局部变量,函数结束局部变量就销毁了,通常情况下是通过malloc去堆上申请一块空间来存储数据。节点创建好后,就要想办法把此节点链接到原链表的最后,因此接下来要先找到原链表的最后一个节点,然后让最后一个节点的指针域存放新创建节点的地址,这样就实现了链接,下面还是通过逻辑结构示意图和物理结构示意图演示一下尾插的过程:
逻辑结构示意图:
在这里插入图片描述
物理结构示意图:
在这里插入图片描述
 尾插还有一种情况需要注意,那就是当链表为空的时候。上面提到过一个链表为空,意味着链表只有一个节点,并且该节点的地址是0x00000000。首先第一步还是创建节点把要尾插的数据先存储起来,然后呢?把这个节点链接到这个地址是0x00000000的节点后面嘛?答案是不行的因为0x00000000所在的地址空间是不允许我们随意访问的,它属于操作系统严格管控的区域,这就意味着我们无法去访问到这块空间的指针域然后把新节点的地址存进去。
 对于空链表尾插的正确做法是,直接将新创建的节点当作头节点。这就意味着:需要把头指针中存放的地址修改成新创建节点的地址!,要记得尾插的所有操作都是封装成函数来实现的,对于函数来说形参的改变不会影响实参,而这里需要修改一个指针变量里面存放的地址,并且希望在函数里面修改之后,在函数外面依然生效,因此这里我们需要进行址传递,也就是传递头指针的地址地址的地址那形参自然就需要用一个二级指针来接收,这里记作pphead。注意:这个二级指针不能为空,因为他存的是指向头节点指针的地址,如果为空那就说明没有这个指向头结点的指针,那就说明链表不存在,要区分链表不存在空链表各自是如何表示的。

  • 空链表:头指针为空,也就是pList == NULL表示的是一个空链表,有些操作时允许空链表的情况的,比如说遍历、尾插、头插数据,而有些情况则不允许出现空链表的情况,比如说头删、尾删数据。因此我们需要根据具体的情况去做检查。
  • 链表不存在pphead ==NULL则是说明链表不存在,对链表的操作,链表不存在当然是不被允许的,所以只要使用了pphead就要对他进行检查。
    代码实现:
void SLTPushBack(SLTNode** pphead,SLTDataType x)
{
	assert(pphead);//pphead不能为空,pphead为空说明里面没有存指向头节点指针的地址,那就说明没有链表
	SLTNode* newnode = BuySLTNode(x);//由于在头插和随机插入的过程中也会涉及到节点的创建,所以这里把节点的创建单独封装了一个函数

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next != NULL)//假如链表为空这里就非法访问了,因此要先判断
		{
			cur = cur->next;
		}
		cur->next = newnode;
	}
}

2.4 创建新节点

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

2.5 头插

 头插显然是要改变头指针存放的地址,因此形参也需要传递二级指针。头插无需单独考虑空链表的情况
代码实现:

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

2.6 尾删

 尾删先要遍历一遍链表找到最后一个节点将其释放掉,还要找到倒数第二个节点将它的指针域中存的地址改为NULL。所以定义两个指针让他们同时去遍历链表,一个找尾,另一个找倒数第二个节点。需要注意的是空链表和只有一个节点的链表的情况,空链表无法进行尾删,而只有一个节点的链表在尾删后会变成一个空链表,这意味着要改变头指针里面存放的地址,所以尾删形参也要传递二级指针。
代码实现:

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);//暴力检查是否为空链表,空链表不能删数据
	//检查链表是否为空
	/*if (*pphead == NULL)//温柔的进行检查
	{
		return;
	}*/
	//只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//有多个节点
	else
	{
		//找尾
		SLTNode* prev = *pphead;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}

		free(tail);
		tail = NULL;

		prev->next = NULL;//假如只有一个节点这里就会非法访问
	}	
}

2.7 头删

 头删很明显需要改变头指针中存放的地址,所以形参仍然需要传递二级指针。头删只需要注意链表是否为空,空链表无法进行删除。此外在进行头删的时候记得将原来的头节点释放掉,因此在改变头节点之前需要先保留原来头结点的地址,否则在更换完新的头节点后就找不到原来的头节点了。
代码展示:

void SLTPopFront(SLTNode** pphead)
{
	if (*pphead == NULL)//这里也可以直接用assert来断言
	{
		return;
	}
	SLTNode* tail = *pphead;
	*pphead = (*pphead)->next;//假如链表为空,这里就会发生越界,因此要判断链表是否为空
	free(tail);
	tail = NULL;
}

2.8 单链表查找

 其实就是遍历一遍链表,但是只能返回第一次出现的地址。查找可以当修改来使用,我们查找到节点的地址后就可以通过地址去修改数据域中存储的数据。

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

2.9 在pos位置之前插入

 和尾插类似,但此时只需要遍历链表找到pos位置的前一个节点即可,同样需要注意pos是头结点的情况,此时就成头插了,需要改变头指针中存的地址,因此函数的形参需要传二级指针。

//在pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = BuySLTNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

 上面这种在pos位置前面插入的方法,需要知道头节点的地址

2.10 删除pos位置数据

 pos可能是头结点的地址,因此形参要用二级指针。

//pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(*pphead);//空链表不能删
	assert(pos);
	if (pos == *pphead)
	{

		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;//其实没用,形参的改变不改变实参
	}
}

2.11 在pos位置的后面插入

 这里需要特别注意地址的赋值顺序。有以下两种正确的赋值顺序供大家参考:

  • 先让newnode的指针域存储pos后一个节点的地址,再让pos的指针域存newnode的地址
  • 借助中间变量先把pos后面节点的地址保存起来,再让pos的指针域存newnode的地址,最后再让newnode的指针域存第一步中间变量中保存的地址。
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* tmp = pos->next;
	pos->next = newnode;
	newnode->next = tmp;
}

2.12 删除pos位置后面的数据

 注意不能写成后面这样:pos->next = pos->next->next。这样写虽然把pos位置后面的节点从链表中剔除出去了,但并没有真正意义上的实现删除,因为每一个节点都是通过malloc在堆上申请的,不使用的时候要主动的去释放掉,也就是free掉,把这块空间归还给操作系统,否则会导致内存泄漏。而上面那样写,就会导致pos后面的节点丢失,无法进行释放,正确的做法是在执行这条语句之前把pos后边节点的地址先保存起来。

void SLTEraseAfter(SLTNode* pos)
//只能删除pos位置后面的节点,不能删除pos节点
//因为pos节点的前一个节点无从得知
{
	assert(pos);
	assert(pos->next);
	SLTNode* tmp = pos->next->next;//这里先保存了pos后面的后面的节点的地址,也是可以的
	free(pos->next);
	pos->next = tmp;
}

 今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!
在这里插入图片描述

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

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

相关文章

Markdown学习

我展示的是一级标题 我展示的是二级标题 斜体文本 斜体文本 粗体文本 粗体文本 粗斜体文本 粗斜体文本 第一项第二项第三项 第一项第二项第三项 第一项第二项第三项 第一项: 第一项嵌套的第一个元素第一项嵌套的第二个元素 第二项: 第二项嵌套的第…

mysql 常见锁类型

表锁 & 行锁 在 MySQL 中锁的种类有很多,但是最基本的还是表锁和行锁:表锁指的是对一整张表加锁,一般是 DDL 处理时使用,也可以自己在 SQL 中指定;而行锁指的是锁定某一行数据或某几行,或行和行之间的…

ROS2 lifecycle node生命周期节点 官方例程文档翻译

0 官方例程文档 ros2 humble lifecycle demo github repo 1 引言 ROS2引入了受管理节点的概念,受管理的节点又被称为是生命周期节点(LifecycleNode)。在接下来的教程中,我们会解释这些节点的作用,是什么使得它们和普…

【Linux 驱动篇(三)】新字符设备驱动

文章目录 一、新字符设备驱动原理1. 分配和释放设备号2. 新的字符设备注册方法2.1 字符设备结构2.2 cdev_init 函数2.3 cdev_add 函数2.4 cdev_del 函数 二、自动创建设备节点1. mdev 机制2. 创建和删除类3. 创建设备4. 参考示例5. 设置文件私有数据 三、LED 灯驱动程序编写四、…

Q-Learning 原理干货讲解

强化学习 本文是强化学习和Q-Learning算法的概念及原理 项目实战案例可查看下一篇文章:Q-Learning 单路径吃宝箱问题–棋盘格吃宝箱问题–拓扑节点较优路径问题 一. 强化学习概述 1.1 什么是强化学习 基于环境的反馈而行动,通过不断与环境的交互、试错&…

AR急诊急救大平台远程专家会诊方案

随着AR技术的完善,在医院急救中,需要及时把各方面专家汇在一起会诊,在AR这方面有数据采集清晰,资料呈现方便便捷,针对客户需要,我们对业务需求分析,比较市面多种产品,把会议&#xf…

Spring Boot 中的 @Cacheable 注解

Spring Boot 中的 Cacheable 注解 在 Spring Boot 中,缓存是一个非常重要的话题。当我们需要频繁读取一些数据时,为了提高性能,可以将这些数据缓存起来,避免每次都从数据库中读取。为了实现缓存,Spring Boot 提供了一…

Apikit 自学日记:发起文档测试-TCP/UDP

进入某个TCP/UDP协议的API文档详情页,点击文档上方 测试 标签,即可进入 API 测试页,系统会根据API文档的定义的求头部、Query参数、请求体自动生成测试界面并且填充测试数据。 填写/修改请求参数 1.1设置请求参数 与发起HTTP协议测试类似&am…

Django Vue corsheaders跨域问题

跨域问题 记录一下在我自己的django-vue项目里面出现的跨域问题 我的项目之前一直是在本地跑的,因为需要上线测试,所以我就运行在同一个vlan里面 ip段:192.168.1.0/24 突然发现存在跨域问题,我django的接口访问被拦截了。 检查…

Web服务器群集:Nginx+Tomcat实现负载均衡与动静分离集群

目录 一、理论 1.多实例 2.Nginx负载均衡 3.Nginx动静分离 4.配置NginxTomcat负载均衡 5.配置NginxTomcat动静分离集群 6.Nginx 四层代理配置 二、实验 1.配置NginxTomcat负载均衡 2.、配置NginxTomcat动静分离集群 三、问题 1.服务器群集与集群的区别 四、总结 一…

十分钟实现 Android Camera2 视频录制

1. 前言 因为工作中要使用Android Camera2 API,但因为Camera2比较复杂,网上资料也比较乱,有一定入门门槛,所以花了几天时间系统研究了下,并在CSDN上记录了下,希望能帮助到更多的小伙伴。 上两篇文章们使用…

Python主动抛出Warning的艺术:一种提醒用户的优雅方式

简介:Python提供了一个内置的warnings模块,使得开发者可以在代码中主动地发出警告。这对于告知用户某些问题或者对某些即将废弃的特性进行提示尤其有用。本文将重点介绍如何在Python代码中主动抛出警告,并探讨其使用场景和优势。 历史攻略&a…

设计模式3:单例模式:volatile关键字能不能解决多线程计数问题?

先说结论不能: 代码实测下: public class Counter {public volatile static int count 0;public static void inc() {//这里延迟1毫秒,使得结果明显try {Thread.sleep(1);} catch (InterruptedException e) {}count;}public static void ma…

Win10安装CUDA

一、安装Nvidia显卡驱动 安装Nvidia显卡驱动前可以先检查Nvidia显卡驱动是否已安装。搜索 Nvidia控制面板 或 Nvidia Control Panel可以看到当前已经安装的显卡驱动及版本。 如需安装显卡驱动,在官方驱动下载网站找到自己的显卡型号对应的驱动下载并安装:官方驱动…

行业分析| 无人机电力巡检技术的应用

随着现代生活水平的不断提升,人们对各行各业的发展都提出了更高的品质要求,对于电力的需求不断上涨,因此也加速了电力行业的转型升级。基于这一发展状况,我国电力行业逐渐开始选择应用无人机电力巡检等现代高科技技术。 无人机电…

Baumer工业相机堡盟工业相机如何通过BGAPISDK进行定序器编程:VCXG双快门操作(C#)

Baumer工业相机堡盟工业相机如何通过BGAPISDK进行定序器编程:VCXG双快门操作(C#) Baumer工业相机Baumer工业相机BGAPISDK和定序器编程的技术背景Baumer工业相机通过BGAPISDK进行定序器编程功能1.引用合适的类文件2.Baumer工业相机通过BGAPISDK进行定序器…

Keep上市,打响健身科技第一炮?

近些年,大众对于身体健康和审美的需求越来越旺盛,因此也引发了一场无形的健身革命。无论是线下动辄大几千的健身房,还是线上的健身直播经济都受到了不小的关注,在疫情刚开始的那段时间,各地的封控让在线健身成为了一种…

修改滚动条样式 和 那些高度

一、滚动条样式 二、那些高度 网页可见区域宽: document .body.clientWidth; 网页可见区域高: document .body.clientHeight; 网页可见区域宽: document .body.offsetWidth (包括边线的宽); 网页可见区域高: document .body.of…

亚马逊实践 | 构建可持续发展的架构模型

可持续发展概念源于对系统性文明危机和世界问题的科学和社会意识形态研究。世界级的进步学术社群和政治精英在二十世纪末就认识到了这些问题的存在。他们将即将到来的二十一世纪视为充满不确定性、全球灾难进程逐步升级的时代。可持续发展对多个领域产生影响,目前已…

Sudo堆溢出漏洞(CVE-2021-3156)复现

背景介绍 2021 年 1 月 26 日,Qualys Research Labs在 sudo 发现了一个缺陷。sudo 解析命令行参数的方式时,错误的判断了截断符,从而导致攻击者可以恶意构造载荷,使得sudo发生堆溢出,该漏洞在配合环境变量等分配堆以及…