单链表(C语言详细版)

news2025/1/11 5:23:15

1. 链表的概念及结构

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

链表的结构跟火车车厢相似,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉或加上,不会影响其他车厢,每节车厢都是独立存在的。

车厢是独立存在,且每节车厢都有车门。想象一下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带一把钥匙的情况下如何从车头走到车尾?

最简单的做法:每节车厢里都放一把下一节车厢的钥匙。

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

与顺序表不同的是,链表里的每节“车厢”都是独立申请下来的空间,我们称之为“结点/节点”。节点的组成主要有两个部分:当前节点要保存的数据和保存下一个节点的地址(指针变量)。图中指针变量 plist 保存的是第一个节点的地址,我们称 plist 此时“指向”第一个节点,如果我们希望 plist “指向”第二个节点时,只需要修改 plist 保存的内容为 0x0012FFA0。

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

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

假设当前保存的节点为整型:

// 定义节点的结构
// 数据 + 指向下一个节点的指针
typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;		// 存储的节点数据
	struct SListNode* next; // 指针变量用来保存下一个节点的地址
}SLTNode;

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,也需要保存下一个节点的地址(当下一个节点为空时保存的地址为空)。

当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一个节点的地址(下一个节点的钥匙)就可以了。

给定的链表结构中,如何实现节点从头到尾的打印?

补充:

1. 链式结构在逻辑上是连续的,在物理结构上不一定连续;

2. 节点一般是从堆上申请的;

3. 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能是连续,可能不连续。

2. 单链表的实现

2.1 链表的打印

// 链表的打印
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)	// pcur != NULL
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

测试程序:首先我们得先开辟新的空间,然后创建几个新的节点,再把所创建的新节点关联起来,最后调用打印函数接口,打印数据(注意:最后一个节点存储的下一个节点的地址要为NULL

void SListTest01()
{
	// 链表是由一个一个的节点组成
	// 创建几个节点
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;

	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;

	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;

	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;

	// 将四个节点连接起来
	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	// 调用链表的打印
	SLTNode* plist = node1;
	SLTPrint(plist);
}

int main()
{
	SListTest01();

	return 0;
}

运行结果:

2.2 链表的尾插

// 链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);

如果我们要在链表的最后一个节点尾插一个新的节点,首先要先找到尾节点,再将尾节点和新节点连接起来。不过,在插入之前我们得先申请一个新的节点空间。这里需要注意的一个点:我们形参要改变实参必须要传地址(即要用指针来接收),那为什么形参不用一级指针而用二级指针呢?因为,我们创建的节点是一个结构体指针 SLTNode* ,所以我们得用二级指针来接收。

知道了原理,我们接下来就可以编写程序,首先得判断头节点的地址 &plist (pphead)不能为空,然后申请一个新的节点空间。好,接下来我们要分两种情况:1. 链表是空链表;2. 链表是非空链表。空链表:我们直接把新节点作为头节点;非空链表:我们就正常的尾插。

尾插接口函数:

// 申请节点函数
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newNode == NULL)
	{
		perror("malloc fail!");
		exit(1);	// 正常退出是0,非正常退出是非0
	}
	newNode->data = x;
	newNode->next = NULL;

	return newNode;
}

// 链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);	// pphead 不能为NULL
	// *pphead 就是指向第一个节点的指针
	// 空链表和非空链表
	// 申请新的节点
	SLTNode* newNode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newNode;
	}
	else
	{
		// 找尾
		SLTNode* ptail = *pphead;
		while (ptail->next)	// ptail->next != NULL
		{
			ptail = ptail->next;
		}
		// ptail指向的就是尾节点
		ptail->next = newNode;
	}
}

测试程序:尾插 1 2 3 4

void SListTest02()
{
	SLTNode* plist = NULL;
	// 测试尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
}

int main()
{
	SListTest02();

	return 0;
}

运行结果:

2.3 链表的头插

// 链表的头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);

头插我们也得分两种情况:1. 链表是空链表;2. 链表是非空链表。

首先得判断头节点的地址 &plist (pphead)不能为空,然后申请一个新的节点空间。让新节点 newNode 的下一个节点的地址 newNode->next 指向头节点 *pphead ,再新节点 newNode 作为新的头节点 *pphead。

头插接口函数:

// 链表的头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	// 空链表和非空链表都能使用此方法
	assert(pphead);	// pphead 不能为NULL
	// 申请新的节点
	SLTNode* newNode = SLTBuyNode(x);
	newNode->next = *pphead;
	*pphead = newNode;
}

测试程序:尾插 1 2 3 4,再头插 6 7 8

void SListTest02()
{
	SLTNode* plist = NULL;
	// 测试尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);

	// 测试头插
	SLTPushFront(&plist, 6);
	SLTPrint(plist);
	SLTPushFront(&plist, 7);
	SLTPrint(plist);
	SLTPushFront(&plist, 8);
	SLTPrint(plist);
}

int main()
{
	SListTest02();

	return 0;
}

运行结果:

我们测试一下空链表是不是也可行:

void SListTest02()
{
	SLTNode* plist = NULL;

	// 测试头插
	SLTPushFront(&plist, 6);
	SLTPrint(plist);
	SLTPushFront(&plist, 7);
	SLTPrint(plist);
	SLTPushFront(&plist, 8);
	SLTPrint(plist);
}

int main()
{
	SListTest02();

	return 0;
}

运行结果:可行!

2.3 链表的尾删

// 链表的尾删
void SLTPopBack(SLTNode** pphead);

链表节点的删除不是单纯把要删除的节点给free掉,而是要找最后一个节点的前一个节点,把这个节点的下一个节点的地址置为NULL。

尾删我们也要分两种情况:1. 只有单节点;2. 多节点。单节点:我们直接把头节点给释放掉,然后置为空。多节点:我们先找到最后一个节点,怎么找呢?定义一个 ptail 结构体指针,遍历链表,当 patil->next 为 NULL ,说明此时的 ptail 就是尾节点。我们还要找尾节点的前一个节点,这时我们就要定义一个 prev 结构体指针,把找尾节点的 prev = ptail 保存一下,最后 patil = NULL,说明前一次找的就是尾节点的前一个节点。

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

	// 链表只有一个节点
	if ((*pphead)->next == NULL)	// -> 优先级高于 *
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		// 链表有多个节点
		SLTNode* prev = *pphead;
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}

测试程序:尾插 1 2 3 4,然后尾删,一直删到只剩一个节点,多节点和单节点一起判断

void SListTest02()
{
	SLTNode* plist = NULL;
	// 测试尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);

    // 测试尾删
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
}

int main()
{
	SListTest02();

	return 0;
}

运行结果:

2.4 链表的头删

// 链表的头删
void SLTPopFront(SLTNode** pphead);

在释放头节点之前,我们要先用一个指针 next 保存头节点 *pphead 下一个节点的地址,然后释放头节点,再把 next 指针作为新的头节点。

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

	// 多个节点和单个节点都能使用
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

测试程序:

void SListTest02()
{
	SLTNode* plist = NULL;
	// 测试尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);

    // 测试头删
	SLTPopFront(&plist);
	SLTPrint(plist);
	SLTPopFront(&plist);
	SLTPrint(plist);
	SLTPopFront(&plist);
	SLTPrint(plist);
	SLTPopFront(&plist);
	SLTPrint(plist);
}

int main()
{
	SListTest02();

	return 0;
}

运行结果:尾插 1 2 3 4,然后头删,一直删到只剩一个节点,多节点和单节点一起判断

2.5 链表的查找

// 链表的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

遍历链表数据,找到了,就返回对应的结构体指针,没有找到,就返回NULL。

// 链表的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

测试程序:

void SListTest02()
{
	SLTNode* plist = NULL;
	// 测试尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);

    // 测试查找
	SLTNode* find = SLTFind(plist, 3);
	if (find == NULL)
	{
		printf("没有找到!\n");
	}
	else
	{
		printf("找到了!\n");
	}
}

int main()
{
	SListTest02();

	return 0;
}

运行结果:

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

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

相关文章

Java面试八股之描述一下MySQL使用索引查询数据的过程

描述一下MySQL使用索引查询数据的过程 1.解析查询语句与查询优化 用户提交一个 SQL 查询语句,MySQL 的查询解析器对其进行词法分析和语法分析,生成解析树。 查询优化器根据解析树、表结构信息、统计信息以及索引信息,决定是否使用 B树索引…

解锁AI大模型潜能:预训练、迁移学习与中间件编程的协同艺术

在人工智能的浩瀚星空中,大型预训练模型(Large Language Models, LLMs)犹如璀璨的星辰,引领着技术革新的浪潮。这些模型通过海量数据的滋养,学会了理解语言、生成文本乃至执行复杂任务的能力。然而,要让这些…

符号同步、定时同步和载波同步

符号同步、定时同步和载波同步是通信系统中重要的同步技术,它们各自承担着不同的功能和作用。以下是对这三种同步技术的详细解释: 符号同步 定义: 符号同步,也称为定时恢复或时钟恢复,是指在数字通信系统中&#xff…

mysql 5.7.44 32位 zip安装

前言 因为研究别人代码,他使用了5.7的 32位 mysql ,同时最新的 8.4 64位 mysql 不能用官方lib连接。所以安装这个版本使用,期间有些坑,在这里记录一下。 下载路径 mysql官方路径:https://downloads.mysql.com/archi…

更深入了解汽车与航空电子等安全关键型应用的IP核考量因素

作者:Philipp Jacobsohn,SmartDV高级应用工程师 中国已经连续十多年成为全球第一大汽车产销国,智能化也成为了汽车行业发展的一个重要方向,同时越来越多的制造商正在考虑进入无人机和飞行汽车等低空设备,而所有的这些…

一周IT资讯 | B站、小红书等应用崩溃,系阿里云服务器异常所致;余承东回西工大演讲,网友:“史上最强招生guang告”

4.B站、小红书等应用崩溃,系阿里云服务器异常所致 7月2日上午,“B站崩了”“小红书崩了”等话题登上热搜。B站APP无法使用浏览历史关注等内容,消息界面、更新界面、客服界面均不可用,用户也无法评论和发弹幕,视频评论…

React+TS前台项目实战(二十六)-- 高性能可配置Echarts图表组件封装

文章目录 前言CommonChart组件1. 功能分析2. 代码详细注释3. 使用到的全局hook代码4. 使用方式5. 效果展示 总结 前言 Echarts图表在项目中经常用到,然而,重复编写初始化,更新,以及清除实例等动作对于开发人员来说是一种浪费时间…

浏览器开发者视角及CSS表达式选择元素

点击想要查看的接口,然后点击检查,便可以切换到该接口对应的html代码 如果F12不起作用的话,点击更多工具,然后选择开发者工具即可 ctrlF可以去查阅相关的CSS表达式选择元素 如果没有加#t1,那么表示的是选择所有的p 使用…

对比学习和多模态任务

1. 对比学习 对比学习(Contrastive Learning)是一种自监督学习的方法,旨在通过比较数据表示空间中的不同样本来学习有用的特征表示。其核心思想是通过最大化同类样本之间的相似性(或降低它们之间的距离),同…

使用F1C200S从零制作掌机之debian文件系统完善NES

一、模拟器源码 源码:https://files.cnblogs.com/files/twzy/arm-NES-linux-master.zip 二、文件系统 文件系统:debian bullseye 使用builtroot2018构建的文件系统,使用InfoNES模拟器存在bug,搞不定,所以放弃&…

这8款宝藏软件,才是安卓手机必装App!

​AI视频生成:小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频https://aitools.jurilu.com/ 1.我的日记——My Diary My Diary 是一款带锁的免费安卓日记工具。 它可用于记录每日日记、秘密想法、旅程、心情追踪或任何私人时刻。 你可…

使用AI学习英语

使用AI学英语可以通过与智能AI对话、模拟对话场景、提供即时反馈和个性化学习计划等方式提高学习效率和效果。然而,AI技术也存在局限性,如缺乏情感交流和真实语境,需要与真人教师结合使用。 AI学英语的基本原理和应用 AI的基本原理 AI&…

Java内存区域与内存溢出异常(补充)

2.2.5 方法区 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一…

设计模式探索:适配器模式

1. 适配器模式介绍 1.1 适配器模式介绍 适配器模式(adapter pattern)的原始定义是:将一个类的接口转换为客户期望的另一个接口,适配器可以让不兼容的两个类一起协同工作。 适配器模式的主要作用是把原本不兼容的接口&#xff0c…

采用3种稀疏降噪模型对心电信号进行降噪(Matlab R2021B)

心电信号采集自病人体表,是一种无创性的检测手段。因此,心电信号采集过程中,本身也已经包含了机体内部其他生命活动带来的噪声。同时,由于采集设备和环境中存在电流的变化,产生电磁发射等物理现象,会对心电…

3-6 构建线性模型解决温度计示数转换问题

3-6 构建线性模型解决温度计示数转换问题 直接上源码 %matplotlib inline import numpy as np import torch torch.set_printoptions(edgeitems2, linewidth75)导入必要的库并设置 PyTorch 的打印选项,确保在打印张量时显示边缘项和行宽。 #%% t_c [0.5, 14.0,…

【Android应用】生成证书和打包

安卓生成证书和打包 📖1. 生成自有证书📖2. 安卓打包✅步骤一:导入签名文件✅步骤二:设置打包版本✅步骤三:生成签名包或APK 📖1. 生成自有证书 地址:https://www.yunedit.com/createcert 说明…

C语言编译报错error: expected specifier-qualifier-list before

C语言编译报错 error: storage class specified for parameter error: expected specifier-qualifier-list before 原因: 报错信息 "expected specifier-qualifier-list" 通常表示编译器期望在某个地方出现类型指定列表,但却没有找到。这通常…

【目标检测】使用自己的数据集训练并预测yolov8模型

1、下载yolov8的官方代码 地址: GitHub - ultralytics/ultralytics: NEW - YOLOv8 🚀 in PyTorch > ONNX > OpenVINO > CoreML > TFLite 2、下载目标检测的训练权重 yolov8n.pt 将 yolov8n.pt 放在ultralytics文件夹下 3、数据集分布 注…

【嵌入式DIY实例-ESP8266篇】-LCD ST7735显示BME280传感器数据

LCD ST7735显示BME280传感器数据 文章目录 LCD ST7735显示BME280传感器数据1、硬件准备与接线2、代码实现本文中将介绍如何使用 ESP8266 NodeMCU 板(ESP12-E 模块)和 BME280 气压、温度和湿度传感器构建气象站。 NodeMCU 微控制器 (ESP8266EX) 从 BME280 传感器读取温度、湿度…