【数据结构】【版本1.1】【线性时代】——单链表



快乐的流畅:个人主页


个人专栏:《算法神殿》《数据结构世界》《进击的C++》

远方有一堆篝火,在为久候之人燃烧!

文章目录

  • 引言
  • 一、顺序表的问题
  • 二、链表的概念
  • 三、单链表的模拟实现
    • 3.1 定义
    • 3.2 打印
    • 3.3 创建新节点
    • 3.4 头插
    • 3.5 尾插
    • 3.6 头删
    • 3.7 尾删
    • 3.8 查找与修改
    • 3.9 指针断言
    • 3.10 指定插入
    • 3.11 指定删除
    • 3.12 销毁

引言

数据结构世界——单链表(Single Linked List)

一、顺序表的问题

让我们回顾一下顺序表:

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

那么,如何解决以上问题呢?

二、链表的概念

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


  • 链表的结构跟火车车厢相似,将火车里的某节车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。
  • 车厢是独立存在的,且每节车厢都有车门。想象一下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带一把钥匙的情况下如何从车头走到车尾?
  • 最简单的做法:每节车厢里都放一把下一节车厢的钥匙

在链表里,每节“车厢”是怎样的呢?

  • 与顺序表不同的是,链表的每节"车厢"都是独立申请下来的空间,我们称之为结点/节点
  • 节点的组成主要有两个部分:当前节点要保存的数据和保存下一个节点的地址(指针变量)

图中指针变量 plist保存的是第一个节点的地址,我们称plist此时“指向”第一个节点,如果我们希望plist“指向”第二个节点时,只需要修改plist保存的内容为0x0012FFA0


为什么还需要指针变量来保存下一个节点的位置?

链表中每个节点都是独立申请的(即需要插入数据时才去申请一块节点的空间),我们需要通过指针变量来保存下一个节点位置才能从当前节点找到下一个节点

三、单链表的模拟实现

3.1 定义

//单链表
typedef int SLDataType;
typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SLNode;
  • 在结构体内嵌套结构体,一定要表明struct,而不能直接使用typedef后的名称

3.2 打印

先来看看怎么实现链表的打印,这样更有助于我们理解它的结果。

void SLPrint(SLNode* phead)
{
	SLNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}
  • 先用cur指针接受头部地址,当cur不为NULL时,则打印当前节点的数据,并将该节点存储的下一节点的地址赋值给cur
  • 这样cur指针就能一直访问整个链表,直到最后一个节点(该节点存储地址为NULL

3.3 创建新节点

每次插入,要生成新节点,申请空间……一系列操作 ,所以我们可以把它该过程写成一个函数,以增强函数的复用性

SLNode* BuySLNode(SLDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}
  • malloc动态开辟内存生成一个新节点,再将数据放入新节点中,初始地址为NULL

3.4 头插

void SLPushFront(SLNode** pphead, SLDataType x)
{
	assert(pphead);
	SLNode* newnode = BuySLNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
  • 先创建新节点,再将新节点链接在链表头部
  • 更新链表指针

可能很多人有疑惑,为什么要用二级指针呢?请看接下来的测试:

与顺序表不同的是,我们不再创建结构体,而是创建结构体指针,初始指向NULL。那么,我们要在函数内改变外部指针指向的地址,就要使用二级指针

3.5 尾插

void SLPushBack(SLNode** pphead, SLDataType x)
{
	assert(pphead);
	//1.空链表
	//2.非空链表
	SLNode* newnode = BuySLNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}
  • 如果为空链表,则直接将新节点的地址传给外部的链表指针
  • 如果为非空链表,先申请新节点,再用循环一直向后访问,找到最后一个节点的地址,将新节点链接在最后

3.6 头删

void SLPopFront(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLNode* cur = *pphead;
	*pphead = (*pphead)->next;
	free(cur);
}
  • 将链表指针的地址改成第二个节点的,并将第一个节点的空间释放

3.7 尾删

void SLPopBack(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	//一个节点
	//多个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
}
  • 如果为一个节点,直接释放空间,并赋值为NULL
  • 如果有多个节点,找到倒数第二个节点,解引用将其next指针指向的空间(最后一个节点)释放,再赋值为NULL

3.8 查找与修改

SLNode* SLFind(SLNode* phead, SLDataType x)
{
	SLNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
  • 找到返回对应节点的地址,找不到返回NULL
  • 能查找,也意味着能修改,因为我们获取了该节点的地址,自然能在外部解引用修改数据,这样,一个函数就有两个功能 ——查找和修改

3.9 指针断言

这里说一下关于assert断言的情况 ,并不是所有函数参数的指针都需要断言,而是要根据实际情况分析而定。


//打印
void SLPrint(SLNode* phead);
//查找
SLNode* SLFind(SLNode* phead, SLDataType x);

打印与查找,则不需要断言,因为空链表也可以打印,也可以查找,就比如你的银行账户没有钱,就不能显示出来看看,不能查询吗?


//头插
void SLPushFront(SLNode** pphead, SLDataType x);
//尾插
void SLPushBack(SLNode** pphead, SLDataType x);

头插与尾插,对于其二级指针需要断言,一级指针不用,因为pphead指向链表指针plist,所以不能为空,而链表指针可为空(即为空链表)。


//头删
void SLPopFront(SLNode** pphead);
//尾删
void SLPopBack(SLNode** pphead);

头删与尾删,其二级指针与一级指针都要断言,除了pphead不能为空,*pphead也不能为空,因为空链表就不能进行删除操作。

3.10 指定插入

这里有两种插入方式,在pos前插入在pos后插入

在pos前插入

void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SLPushFront(pphead, x);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLNode* newnode = BuySLNode(x);
		newnode->next = pos;
		prev->next = newnode;
	}
}
  • 如果pos为头部地址时,即为头插,可复用头插函数
  • 如果pos不为头部地址时,再找到pos前一个节点的地址,然后创建新节点,最后将它们链接起来

在pos后插入

void SLInsertAfter(SLNode* pos, SLDataType x)
{
	assert(pos);
	SLNode* newnode = BuySLNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
  • 相比于在pos前插入,单链表其实更适合在pos后插入,直接创建新节点,进行链接即可

ps:链接的过程有顺序问题,不能写反了,要不然会环状链接。

3.11 指定删除

这里也有两种删除方式,在pos删除在pos后删除

在pos删除

void SLErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SLPopFront(pphead);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}
  • 如果pos为头部地址时,即为头删,可复用头删函数
  • 如果pos不为头部地址时,再找到pos前一个节点的地址,链接pos后一个节点,再释放pos节点空间

在pos后删除

void SLEraseAfter(SLNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLNode* next = pos->next;
	pos->next = pos->next->next;
	free(next);
}
  • 相比于在pos位置删除,单链表其实更适合在pos后删除,这里用一个next指针,保存pos下一个节点的地址,在pos链接往后第二个节点后,再对next节点空间释放

3.12 销毁

void SLDestroy(SLNode** pphead)
{
	assert(pphead);
	SLNode* cur = *pphead;
	while (cur)
	{
		SLNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}
  • 在每次循环内临时创建一个新指针next,记录下一个节点的地址,让cur释放所指空间后,还可以找到下一个节点,最后把链表指针解引用置空

当然,这里你也可以传一级指针,不在函数内部把外部的链表指针置为NULL,而在外部手动置空,跟free函数的用法一样,实现半自动。

void SLDestroy(SLNode* phead)
{
	SLNode* cur = phead;
	while (cur)
	{
		SLNode* next = cur->next;
		free(cur);
		cur = next;
	}
}

真诚点赞,手有余香

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android MediaMetadataRetriever获取视频宽高,Java

Android MediaMetadataRetriever获取视频宽高,Java public static int[] getVideoSize(Context ctx, Uri uri) {MediaMetadataRetriever retriever new MediaMetadataRetriever();int[] size {-1, -1}; //宽,高try {retriever.setDataSource(ctx, uri)…

JVC摄像机SD卡变成RAW的恢复方法

JVC小日本胜利公司,公司名字绕口且产品线极广,涉及汽车、影音、娱乐……,而JVC在摄像机产品方面也有涉及,不过市场上极为少见。下边我们来看下这个JVC摄像机MP4恢复案例。 故障存储: 32G存储卡 RAW文件系统 故障现象: 客户无…

Linux Radix tree简介

文章目录 前言一、Radix tree简介二、Operations2.1 Lookup2.2 Insertion2.3 Deletion 三、Linux内核API3.1 初始化3.2 radix_tree_insert/delete3.3 radix_tree_preload3.4 radix_tree_lookup3.5 radix_tree_tag_set3.6 radix_tree_tagged 四、address_space4.1 简介4.2 相应数…

新一代大核卷积反超ViT和ConvNet!同参数量下性能、精度、速度完胜

大核卷积网络是CNN的一种变体,也是深度学习领域的一种重要技术,它使用较大的卷积核来处理图像数据,以提高模型对视觉信息的理解和处理能力。 这种类型的网络能够捕捉到更多的空间信息,因为它的大步长和大感受野可以一次性覆盖图像…

34万汉语词语成语反义词ACCESS\EXCEL数据库

反义词就是两个意思相反的词,包括:绝对反义词和相对反义词。分为成对的意义相反、互相对立的词。如:真——假,动——静,拥护——反对。这类反义词所表达的概念意义互相排斥。或成对的经常处于并举、对待位置的词。如&a…

WinForm之TCP服务端

目录 一 原型 二 源码 一 原型 二 源码 using System.Net; using System.Net.Sockets; using System.Text;namespace TCP网络服务端通讯 {public partial class Form1 : Form{public Form1(){InitializeComponent();}TcpListener listener null;TcpClient handler null;Ne…

记C#优化接口速度过程

前提摘要 首先这个项目是接手的前一任先写的项目,接手后,要求对项目一些速度相对较慢的接口进行优化,到第一个速度比较慢的接口后,发现单接口耗时4-8秒,是的,请求同一个接口,在参数不变的情况下…

如何在CST软件中获得多天线不同频的SAR

之前写过计算SAR的文章,但是没有提到多天线的情况。 仿真实例018:均匀头模型和天线SAR比吸收率仿真案例 CST软件如何用E场计算Loss损耗密度 --- SAR计算加速技巧 这期我们看看多天线不同频率如何计算SAR。 用一个简单的手模型和三个不同长度天线为例&a…

红海云签约盛帆集团,开启多元化集团人力资源数字化新征程

武汉盛帆投资集团股份有限公司(以下简称“盛帆集团”)是以能源管理产业为根本,以金融投资产业为纽带,以文体产业为拓展方向的多元化集团企业。公司能源管理产业创立于1998年,主要从事智能电表、智能水表、集中器、高压…

学习笔记——网络管理与运维——SNMP(概述)

一、SNMP概述 1、SNMP背景 SNMP的基本思想:为不同种类的设备、不同厂家生产的设备、不同型号的设备,定义为一个统一的接口和协议,使得管理员可以是使用统一的外观面对这些需要管理的网络设备进行管理。 通过网络,管理员可以管理…

NewspaceAi之GPT使用新体验

GPT功能 使用地址:https://newspace.ai0.cn/ 上车 挂挡 踩油门,一脚到底,开始你的表演 问题1:你能做什么详细告诉我? 下面内容是GPT的回答 当然!作为一个基于GPT-4架构的AI,我能够在许多方面为…

Linux基础一

目录 一,Linux中常用的快捷键 二,man指令 三,pwd指令 四,cd指令 五,ls指令 六,mkdir和rmdir指令 七,touch指令 八,cp指令 九,mv指令 十,cat指令 十一&#xf…

React+TS前台项目实战(八)-- 全局常用组件模态框Modal封装

文章目录 前言Modal模态框组件1. 功能分析2. 代码详细注释说明3. 使用方式4. 效果展示 总结 前言 今天这篇主要讲项目中经常会用到的模态框Modal组件封装。模态框可用在很多地方,比如弹窗Dialog使用、消息提示Message使用等都可以在外层套上Modal组件,下…

牛客链表刷题(一)

目录 题目一:反转链表 代码: 题目二:链表内指定区间反转 代码: 题目一:反转链表 代码: import java.util.*;/** public class ListNode {* int val;* ListNode next null;* public ListNode(int …

微信小游戏插件申请,微信小程序插件管理

微信小游戏的插件申请与小程序不一样,官方没有提供一个统一的管理入口进行申请插件,以及查看插件,没有小程序方便的; 小程序申请查看插件入口如下图所示: 小游戏的插件可以通过以下的方式进行申请: 如下…

Python跳动的爱心(双爱心版)

目录 系列文章 前言 Turtle简介 Python跳动的爱心 尾声 系列文章 序号文章目录直达链接表白系列1无法拒绝的表白界面https://want595.blog.csdn.net/article/details/1347448942满屏飘字表白代码https://want595.blog.csdn.net/article/details/1350373883无限弹窗表白代…

微信小程序查分易如何使用?

期末马上到了,老师们又开始为发放成绩而头疼了,堆积如山的试卷,密密麻麻的分数,还有那些不断响起的家长电话,真是让人心烦。别担心,今天就让我来介绍一个让老师“偷懒”神器——查分易微信小程序 第一步&am…

汇编:宏的使用

汇编语言中的宏是用于定义可重复使用的代码块或指令集合的强大工具。宏通过简化代码编写和提高可读性,使得编写和维护汇编程序更加方便;在 MASM(Microsoft Macro Assembler)中,宏的定义和使用非常常见。以下是对汇编语…

windows环境如何运行python/java后台服务器进程而不显示控制台窗口

1.通常我们在windows环境下使用Java或Python语言编写服务器程序,都希望他在后台运行,不要显示黑乎乎的控制台窗口: 2.有人写了一个bat文件: cd /d D:\lottery\server && python .\main.py 放到了开机自启动里,可是开机的…

鹧鸪云光伏业务管理系统,助力企业数智化发展

在当今数字化浪潮席卷全球的背景下,光伏行业作为绿色能源的重要组成部分,其业务管理的数智化转型显得尤为重要。鹧鸪云光伏业务管理系统,以其强大的功能和卓越的性能,正成为企业实现数智化转型的重要助力。 作为光伏行业的领军软…