【数据结构—单链表的实现】

news2024/11/17 1:56:35

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

前言

1. 链表的概念及结构

2. 单链表的实现

2.1单链表头文件——功能函数的定义

2.2单链表源文件——功能函数的实现

2.3 单链表源文件——功能的测试

3.具体的理解操作图

4. 链表的分类

总结


前言

世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!


提示:以下是本篇文章正文内容,下面案例可供参考

1. 链表的概念及结构

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

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

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

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

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

与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为“结点/节点”

节点的组成主要有两个部分:当前节点要保存的数据和保存下一个节点的地址(指针变量)。

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

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

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

结合前面学到的结构体知识,我们可以给出每个节点对应的结构体代码:

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

struct SListNode
{
 int data; //节点数据
 struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};

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

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

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

思考:当我们想保存的数据类型为字符型、浮点型或者其他自定义的类型时,该如何修改?

补充说明:

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

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

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

顺序表的缺点:

1:顺序表需要申请的空间是连续的,可能造成程序的消耗;

2:扩容存在一定的空间浪费。

2. 单链表的实现

2.1单链表头文件——功能函数的定义

Slist.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

//定义链表节点的结构:
typedef int SLDatatype;
typedef struct SlistNode
{
	SLDatatype data;//这个节点(结构体)内的数据
	struct SlistNode* next;//存放的是下一个节点(结构体)的地址
}SLNode;

//我们来创建几个节点组成一个链表,并打印链表
//phead:第一个节点的地址
void SLprint(SLNode* phead);

//尾插
void SLPushBack(SLNode** pphead, SLDatatype x);
//头插
void SLPushFront(SLNode** pphead, SLDatatype x);
//尾删
void SLPopBack(SLNode** pphead);
//头删
void SLPopFront(SLNode** pphead);

//找节点,这里的第一个参数是一级指针还是二级指针
//这里穿一级指针实际就可以了,因为不改变节点
//但是这里要写二级指针,因为要保持接口的一致性
SLNode* SLFind(SLNode** pphead, SLDatatype x);

//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDatatype x);
//在指定位置之后插入数据
void SLInsertAfter(SLNode* pos, SLDatatype x);

//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos);
//删除pos节点之后
void SLEraseAfter(SLNode* pos);

//链表的销毁
void SLDesTroy(SLNode** pphead);

2.2单链表源文件——功能函数的实现

Slist.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Slist.h"
//顺序表在逻辑上是线性的,在物理空间上也是线性的,顺序表中的数据都是连续的
//链表在逻辑结构上是线性的,在物理空间上不是线性的,链表中的数据不是连续的
//一个节点中存放两个东西:1:存储的数据;2:下一个节点的地址

链表节点的结构:
//struct SlistNode
//{
//	int data;//这个节点(结构体)内的数据
//	struct SlistNode* next;//存放的是下一个节点(结构体)的地址
//};

void SLprint(SLNode* phead)//phead:是形参,指向第一个节点的地址
{
	//循环打印
	//可以使用phead直接来访问,但是注意,当代码走到
	//第27行的时候,此时的phead就是NULL了,
	//若代码没有写完,我们还要继续使用指向第一个节点的地址时,
	//这时我们就找不到第一个节点的地址了
	SLNode* pcur = phead;//申请一个变量,用来存放第一个节点的地址
	while (pcur != NULL)
	{
		printf("%d ->", pcur->data);
		pcur = pcur->next;//pcur:是第一个节点的地址,在节点中找到下一个节点的地址,赋值给pcur
	}
	printf("NULL\n");
}

//申请新节点插入数据
SLNode* SLBuyNode(SLDatatype x)
{
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
	node->data = x;
	node->next = NULL;
	return node;
}
//尾插
void SLPushBack(SLNode** pphead, SLDatatype x)
{
	//对指针加以限制
	assert(pphead);//传过来的指针不能为空
	SLNode* node = SLBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = node;
		return;
	}
	//当链表不为空时,找尾
	SLNode* pcur = *pphead;
	while (pcur->next)
	{
		pcur = pcur->next;
	}
	pcur->next = node;
}

//头插
void SLPushFront(SLNode** pphead, SLDatatype x)
{
	assert(pphead);
	//链表为空和不为空的代码是一样的
	SLNode* node = SLBuyNode(x);
	//让新节点跟头节点连接起来
	node->next = *pphead;//*pphead:第一个节点的地址
	//让新的节点成为头节点
	*pphead = node;
}

//尾删
void SLPopBack(SLNode** pphead)
{
	assert(pphead);
	//第一个节点不能为空(链表为空,不能尾删)
	assert(*pphead);
	//只有一个节点的情况下
	if ((*pphead)->next == NULL)
	{
		//直接把头节点删除
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
	    //有多个节点的情况下
	   //找到尾节点和尾节点的前一个节点
		SLNode* prev = NULL;
		SLNode* ptail = *pphead;//第一个节点的地址
		while (ptail->next != NULL)
		{
			prev = ptail;
			ptail = ptail->next;
			//当ptail->next == NULL的时候,prev刚好是尾节点的前一个节点
		}
		//prev的next指针不在指向ptail,而是指向ptail的下一个节点
		prev->next = ptail->next;//next指向NULL
		free(ptail);
		//为什么必须要置为空?代码后面明明没有在使用ptail?
		ptail = NULL;
		//因为打印的时候会判断地址是否为空,如果不置为空,那么会造成非法访问
	}
}
//头删
void SLPopFront(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	//无论是一个节点或者是多个节点都是可以的
	SLNode* del = *pphead;//第一个节点的地址
	*pphead = (*pphead)->next;//->的优先级高于*
	free(del);
	del = NULL;//出于代码规范的写法
}

//查找第一个为x的节点
SLNode* SLFind(SLNode** pphead, SLDatatype x)
{
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}


//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDatatype x)
{
	assert(pphead);
	//我们约定链表不能为空,pos也不能为空(链表为空,pos也绝对为空)
	assert(*pphead);
	assert(pos);
	SLNode* node = SLBuyNode(x);
	//只有一个节点的情况下(pos必须指向第一个节点的位置)
	//pos刚好是头节点,头插
	if ((*pphead)->next == NULL || pos == *pphead)
	{
		node->next = *pphead;
		*pphead = node;
		return;
	}
	//找pos的前一个节点
	SLNode* prev = *pphead;
	while (prev->next != pos)//第一次循环的时候(缺少pos刚好是第一个节点的位置)
	{
		prev = prev->next;
	}
	prev->next = node;
	node->next = pos;
}
//在指定位置之后插入数据
void SLInsertAfter(SLNode* pos, SLDatatype x)
{
	assert(pos);
	SLNode* node = SLBuyNode(x);
	node->next = pos->next;
	pos->next = node;
}

//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	//pos刚好是头节点
	if (pos == *pphead)
	{
		*pphead = (*pphead)->next;
		free(pos);
		pos = NULL;
		return;
	}
	//当pos不是头节点,找pos的前一个节点
	SLNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;//只是规范
}
//删除pos节点之后
void SLEraseAfter(SLNode* pos)
{
	//pos节点不能为空,pos之后的节点不能为空
	assert(pos && pos->next);
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

//链表的销毁
void SLDesTroy(SLNode** pphead)
{
	assert(pphead);
	SLNode* pcur = *pphead;
	//循环删除
	while (pcur)
	{
		SLNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

2.3 单链表源文件——功能的测试

test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "Slist.h"

void slttest()
{
	SLNode* node1 = (SLNode*)malloc(sizeof(SLNode));
	node1->data = 1;
	SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));
	node2->data = 2;
	SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));
	node3->data = 3;
	SLNode* node4 = (SLNode*)malloc(sizeof(SLNode));
	node4->data = 4;
	
	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	//打印链表
	SLNode* plist = node1;
	SLprint(plist);
}
//检查一下写的代码是否正确
void slttest01()
{
	//plist:是第一个节点的地址
	SLNode* plist = NULL;
	//尾插
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);
	SLprint(plist);
	//尾删
	//SLPopBack(&plist);
	//SLPopBack(&plist);
	//SLPopBack(&plist);
	//SLPopBack(&plist);
	//SLprint(plist);

	头删
	//SLPopFront(&plist);
	//SLPopFront(&plist);
	//SLPopFront(&plist);
	//SLPopFront(&plist);

	//SLNode* find = SLFind(&plist, 2);
	//SLInsert(&plist, find, 11);
	//SLInsertAfter(find, 100);
	SLDesTroy(&plist);
	SLprint(plist);
}

int main()
{
	//slttest();
	slttest01();
	return 0;
}

3.具体的理解操作图

4. 链表的分类

链表的结构非常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

单链表 和 双向带头循环链表。

1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。 


总结

好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。

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

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

相关文章

Selenium 学习(0.14)——软件测试之测试用例设计方法——因果图法2【基本步骤及案例】

1、因果图法的基本步骤 2、案例分析 1&#xff09;分析原因和结果 2&#xff09;关联原因和结果 投入1元5角或2元&#xff0c;按下“可乐”&#xff0c;送出“可乐”【暂时忽略找零】 投入2元&#xff0c;按下“可乐”或“雪碧”。找零5角&#xff0c;送出“可乐”或“雪…

vue中keep-alive的使用

什么是keep-alive&#xff1f; keep-alive是一个内置组件&#xff0c;用于缓存和管理组件的状态。 当 keep-alive包裹一个组件时&#xff0c;这个组件的状态将会被缓存起来&#xff0c;而不是每次重新渲染。这在多个视图之间切换时特别有用&#xff0c;可以避免重复的创建和销…

uniapp前端+python后端=微信小程序支付到底怎么开发???国内的资料为什么没一篇能讲清楚,简简单单的只需要3步就可以了-V2版本

一.微信小程序支付 真的&#xff0c;在接到这个任务的时候&#xff0c;本以为很简单&#xff0c;不就是普通的浏览器复制粘贴&#xff0c;最不济找下gpt给生成一下&#xff0c;但是到实际开发就不同了&#xff0c;不是后端出问题就是前端&#xff0c;搜资料&#xff0c;上百度…

【Rust】基本的语法概念

Rust初学习 常见概念变量与可变性变量常量隐藏 数据类型标量类型字符类型复合类型元组数组 函数参数语句和表达式具有返回值的函数 注释控制流使用循环重复执行 常见概念 变量与可变性 变量 fn main() {let x 5;println!("The value of x is: {x}");x 6;println…

AlphaFold的原理及解读

1、背景 蛋白质是生物体内一类重要的生物大分子&#xff0c;其结构复杂多样&#xff0c;蛋白质的结构对于理解其功能和参与的生物学过程具有重要意义。从生物学角度上看&#xff0c;蛋白质的结构可以分为四个层次&#xff1a;初级结构、二级结构、三级结构和四级结构。 初级结…

中英双语大模型ChatGLM论文阅读笔记

论文传送门&#xff1a; [1] GLM: General Language Model Pretraining with Autoregressive Blank Infilling [2] Glm-130b: An open bilingual pre-trained model Github链接&#xff1a; THUDM/ChatGLM-6B 目录 笔记AbstractIntroduction 框架总结1. 模型架构2. 预训练设置3…

MySQL的Linux安装

在MySQL官网下载压缩包MySQL :: Download MySQL Community Server (Archived Versions) 下载完成后将压缩包上传到Linux中。我这里是下的CentOS的压缩包。 并且用的是FinalShell连接工具&#xff0c;可以选择压缩包直接上传。 ​ 上传完毕后&#xff0c;新建mysql文件夹&…

[DASCTF 2023 0X401七月暑期挑战赛] web刷题记录

文章目录 EzFlask方法一 python原型链污染方法二 flask框架静态文件方法三 pin码计算 MyPicDisk方法一 字符串拼接执行命令方法二 phar反序列化 EzFlask 考点&#xff1a;python原型链污染、flask框架理解、pin码计算 源码如下 import uuidfrom flask import Flask, request, …

Linux驱动开发笔记(五):驱动连接用户层与内核层的文件操作集原理和Demo

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/134561660 红胖子网络科技博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬…

OpenVINO异步Stable Diffusion推理优化方案

文章目录 Stable Diffusion 推理优化背景技术讲解&#xff1a;异步优化方案思路&#xff1a;异步推理优化原理OpenVINO异步推理Python API同步和异步实现方式对比 oneflow分布式调度优化优势&#xff1a;实现思路 总结&#xff1a; Stable Diffusion 推理优化 背景 2022年&am…

山西电力市场日前价格预测【2023-11-29】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-11-29&#xff09;山西电力市场全天平均日前电价为275.28元/MWh。其中&#xff0c;最高日前电价为415.78元/MWh&#xff0c;预计出现在17:45。最低日前电价为0.00元/MWh&#xff0c;预计出…

Make Pixels Dance: High-Dynamic Video Generation论文解析

高动态视频生成的新进展 Make Pixels Dance: High-Dynamic Video Generation高动态视频生成的新进展前言视频生成模式摘要论文十问实验数据集定量评估指标消融研究 训练和推理技巧训练技术推理技术 更多的应用 Make Pixels Dance: High-Dynamic Video Generation 高动态视频生…

MySQL在Docker容器中的性能损失分析与优化策略

文章目录 1. Docker容器对MySQL性能的潜在影响1.1. IO性能1.2. 网络性能1.3. 资源隔离 2. 优化策略2.1. 使用本地数据卷2.2. 配置合理的容器网络2.3. 限制容器资源2.4. 使用容器编排工具 3. 性能测试与监控4. 结论 &#x1f389;MySQL在Docker容器中的性能损失分析与优化策略 ☆…

sqli-labs靶场详解(less17-less22)

目录 less-17 less-18 less-19 less-20 less-21 less-22 less-17 修改密码关卡 服务器后端 账号密码都存在数据库中 使用UPDATE进行修改密码 尝试username处 尝试好久尝试不出来应该是对用户名进行了过滤 于是对password进行注入 判断注入点 passwdadmin 报错&#xff1a…

MySQL使用函数和存储过程实现:向数据表快速插入大量测试数据

实现过程 1.创建表 CREATE TABLE user_info (id INT(11) NOT NULL AUTO_INCREMENT,name VARCHAR(20) DEFAULT NULL,age INT(3) DEFAULT NULL,pwd VARCHAR(20) DEFAULT NULL,phone_number VARCHAR(11) DEFAULT NULL,email VARCHAR(255) DEFAULT NULL,address VARCHAR(255) DEF…

【TinyALSA全解析(二)】wav和pcm音频文件格式详解

wav和pcm音频文件格式详解 一、本文的目的二、wav和pcm格式文件介绍三、pcm格式文件解析四、wav文件内容解析4.1 文件内容描述4.2 实战分析 五、如何在各种音频格式之间进行转换 /******************************************************************************************…

技术SEO的基础知识和 10 个最佳实践

你有没有想过导致某些网站在搜索结果中排名比其他网站更好的因素&#xff1f;针对搜索引擎进行优化是关键&#xff08;SEO&#xff09;。SEO&#xff0c;即搜索引擎优化&#xff0c;是一种用于提高网站在搜索引擎中的知名度的方法。技术搜索引擎优化&#xff08;SEO&#xff09…

用CHAT总结费曼学习法的关键

问CHAT&#xff1a;费曼学习法的关键 CHAT回复&#xff1a;费曼学习法是由著名物理学家理查德费曼所发明的一种学习方法&#xff0c;旨在以深入理解为目标&#xff0c;帮助自己学习新的知识和技能。 费曼学习法有四个关键步骤&#xff1a; 1. 学习&#xff1a;首先&#xff0…

服务器运行情况及线上排查问题常用命令

一、top命令 指令行&#xff1a; top返回&#xff1a; 返回分为两部分 &#xff08;一&#xff09;系统概览&#xff0c;见图知意 以下是几个需要注意的参数 1、load average&#xff1a; 系统负载&#xff0c;即任务队列的平均长度。三个数值分别为 1分钟、5分钟、15分…

VSCode Vue 开发环境配置

Vue是前端开发中的重要工具与框架&#xff0c;可以保住开发者高效构建用户界面。 Vue2官方文档&#xff1a;https://v2.cn.vuejs.org/ Vue3官方文档&#xff1a;https://cn.vuejs.org/ Vue的安装和引用 Vue2的官方安装指南&#xff1a;https://v2.cn.vuejs.org/v2/guide/ins…