数据结构(C):玩转链表

 

🍺0.前言

        言C之言,聊C之识,以C会友,共向远方。各位博友的各位你们好啊,这里是持续分享数据结构知识的小赵同学,今天要分享的数据结构知识是链表,在这一章,小赵将会向大家展开聊聊链表。✊

1.链表的概念

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

当然了光靠这个字面上的的理解就想理解链表我感觉还是蛮难的,所以呢,小赵在这里为大家找了一幅图片,方便大家理解 

有人说这不是一个火车吗?怎么会和链表扯上关系,但实际上链表和火车是极其相似的,我在这里为大家画了个链表的图。

 

链表的结构就是一环接着一环的,哪这每一个环是什么呢?其实就是我们的结构体了。我们将结构体的指针放在前一个结构体的里面,来实现彼此之间的互通,从而形成一个链表。

2.链表的分类 

可能看到这个标题有人会疑惑为什么链表还有分类呢?不就是一根链子吗?实际上则不然,相比较我们前面所说的火车,链表的可能性更多,它可以没有火车头,它可以彼此之间相互拉着,你拉着我,我拉着你,它们可以从头到尾,也可以围成一个圈,成一个环。

那么究竟到底有哪几种呢?小赵按照三个特征为他们划分,同时它们可以自由组合。

2.1带头不带头

带头和不带头长啥样呢就是下面这个样子

那带头和不带头有啥区别呢?其区别其实和火车很像,我们知道火车头往往是不载客的,而其实我们的这个头也是不载数据的,当然最主要的区别还是在我们后面使用的方便度上。

2.2单向和双向

其实我觉得单向和双向的例子还蛮好理解的,就像我们谈恋爱一样,你单喜欢她,她不喜欢你,就叫单向。互相喜欢就是双向。 

2.3循环和不循环

循环和不循环其实也相对比较好理解一点,在这里就不多说了。

这三个特征各位可以随意组合都能构成链表,同时各位用这三个基本特征去判断链表,也能让各位准确地判断出这是什么链表。

2.4主要使用的链表 

虽然链表的种类如此之多之杂,但实际上我们正常使用的链表只有下面两个。


说白了,就是一个是白手起家,一个是啥都有的大土豪。白手起家的可能在我们的刷题中是很常见的,因为毕竟是白手起家,难度可能会更大一些。而什么都有这个各位后面会知道真的很爽,但题大多数题目是不会给你这么爽的。所以今天我们聊链表的时候会用我们的穷小子,来做,这样以后做大土豪也更容易一些。

3.链表的实现 

 那么链表该如何实现呢?跟上面的顺序表一样,我们也是从链表的功能增删查改来玩。

3.1申请一个链表

这里我们换一种和之前的顺序表不一样的方式,我们直接用指针去玩,因为链表里面的指针非常多,我们可以直接申请一个指针的链表。

SListNode* BuySListNode(SLTDateType x)
{
	SListNode* a = (SListNode*)malloc(sizeof(SListNode));//创建一小节链表
	if(a==NULL)
	{
		perror("malloc failed");
		return;
	}
	a->data = x;
	a->next = NULL;
    return a;
}

3.2头插和尾插

 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{

	assert(*(pplist));
	SListNode* newnode = BuySListNode(x);//创建一个新节点
	newnode->next = *(pplist);//让新节点接上原链表的头节点
	*(pplist) = newnode;//改变原链表的头节点
}
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(*(pplist));
	SListNode* newnode = BuySListNode(x);
	SListNode* node = *(pplist);//记录原链表的头节点
	while (node->next)//找到原链表的尾节点
	{
		node = node->next;
	}
	node->next = newnode;//插入新的尾巴节点
}

好了,在这个代码中,我们也有一些常见的问题。比如小赵当时学的时候就挺奇怪为啥这里不能直接用我们的指针,而要找双指针呢?其实这个时候就是我们在函数那边的一个知识,或者说是指针那边的知识。

3.2.1函数的形参问题

为了方便大家更深入理解这个问题,小赵会带着大家重新回顾之前的知识,同时加深我们对这一块知识的理解。

首先拿我们最熟悉最经典的Swap函数聊起。

void Swap(int x, int y)
{
	int tmp = x;
    x = y;
	y = tmp;
}
int main()
{
	int a = 5;
	int b = 6;
	Swap(a, b);
	printf("a=%d b=%d" ,a,b);
}

 

按照大多数人的理解方式可能这里就很奇怪了,为啥呢?为啥没有交换呢?

实际上这是我们对函数的形参理解不够透彻导致的。其实你向函数传送一个实参的,那边的形参就会将你的函数里面的值给接受了,如果说的形象一点就是接力跑。如果我们把a的值想象成一个棒子的话,那么a向函数里面传参的过程实际上就是a在把棒子给形参的过程。后面跑的实际是形参,跟你这个实参一点关系都没有,你a就留在了原地。拿这里究竟该如何去改呢?就是用我们的指针的知识。因为指针所携带的是你的地址,他可以通过地址找到你从而改变你。

 我们发现这个过程中我们传输东西的本质其实没有变,我们还是将接力棒给了形参,但是这次的形参里面有我们的地址,那么他进行解地址的时候,就可以访问到我们的实参了,从而实现交换的效果。

3.2.2二级指针问题解决

而我们这里为何会使用起二级指针呢?其实原因跟上面一样,我们发现只有把我们实参的地址传过去才能真正改变我们的实参.那要改变我们的指针,就要把我们指针的地址传过去,可我们发现一个问题,那就是普通的地址,指针是能够接受的。那指针的地址呢?用我们前面的知识我们知道,只有二级指针才能接收一级指针的地址,所以这里我们用了二级指针。如果你还是不太清楚整个的逻辑的关系,没关系,小赵还给各位画了一个图。

 相信有了上面的知识各位再看这个代码就会轻松多了,如果各位还有什么问题,也可以私信小赵哦。

 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);//创建一个新节点
	newnode->next = *(pplist);//让新节点接上原链表的头节点
	*(pplist) = newnode;//改变原链表的头节点
}
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);
	SListNode* node = *(pplist);//记录原链表的头节点
	while (node->next)//找到原链表的尾节点
	{
		node = node->next;
	}
	node->next = newnode;//插入新的尾巴节点
}

3.3头删和尾删

 单链表头删
void SListPopFront(SListNode** pplist)
{
	assert(*(pplist));//防止没有节点
	SListNode* node = *(pplist);//记录原链表的头节点
	*(pplist) = node->next;
	free(node);//释放掉原头节点
	node = NULL;
}
//单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(*pplist);
	SListNode* node = *(pplist);//记录原链表的头节点
	while (node->next && node->next->next)//找到原链表的尾节点的前一个节点
	{
		node = node->next;
	}
	SListNode* end = node->next;//保存尾节点
	node->next = NULL;
	free(end);//释放尾节点
	end = NULL;
}

3.4打印链表

void SListPrint(SListNode* plist)
{
    assert(plist);
	while (plist)
	{
		printf("%d->", plist->data);
		plist = plist->next;
	}
}

3.5查找

SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	assert(plist);
	while (plist)
	{
		if (plist->data == x)
		{
			return plist;
		}
		plist = plist->next;
	}
	return NULL;
}

3.5销毁链表

void SLTDestroy(SListNode** pphead)
{
	SListNode* node = *pphead;
	while (node)//直到为空指针为止
   {
		SListNode* node2 = node;//保留当前指针
		node = node->next;//到下一个节点
		free(node2);//释放之前的那个
		node2 = NULL;
	}
	node = NULL;
}

3.6某个位置插入和删除  

3.6.1前插

void SLTInsert(SListNode** pphead, SListNode* pos, SLTDateType x)
{
	assert(pos);
	SListNode* node = *pphead;
	SListNode* newnode = BuySListNode(x);
	if (pos == *pphead)//如果是头节点特殊处理
	{
		newnode->next = node;
		*pphead = newnode;
		return;
	}
	while ( node->next != pos)//找到pos前一个节点
	{
		node = node->next;
	}
	SListNode* begin = node;//要插入的节点的前一个节点
	SListNode* end =pos;//要插入的节点的后一个节点
	begin->next = newnode;
	newnode->next = end;
}

前插的难度是要比后插大的,其原因就在于前插要考虑可能是头插的情况发生,同时 前插还要通过while去找到前一个节点,这一点导致了其的难度大。

3.6.1后插

void SListInsertAfter(SListNode* pos, SLTDateType x)
{
    assert(pos);
	SListNode* end = pos->next;//保留下一个节点
	SListNode* node = BuySListNode(x);//创建新节点
	pos->next = node;//重新连接
	node->next = end;
}

这个是在我们某一个节点后插入,其的感觉像是什么呢?就像是小赵下面这个画一样。

 

断开原连线,连上新的线。

 3.6.3前删

void SLTErase(SListNode** pphead, SListNode* pos)
{
	assert(pos);
    if (pos == *pphead) return;
	SListNode* node = *pphead;
	if (pos == (*pphead)->next )//如果是头删,特殊处理
	{
		free(node);
		node = NULL;
		*pphead = pos;
		return;
	}
	while (node->next->next  != pos)
	{
		node = node->next;
	}
	SListNode* node2 = node->next ;//找到被删节点的前一个节点
	node->next = pos;
	free(node2);
	node2 = NULL;
}

3.6.4后删

void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	if (pos->next == NULL) return;//处理特殊情况
	SListNode* end = pos->next->next;//保存后面的后面的节点
	SListNode* node = pos->next;
	pos->next = end;
	free(node);
	node = NULL;
}

 4.结束语

好了小赵今天的分享就到这里了,如果大家有什么不明白的地方可以在小赵的下方留言哦,同时如果小赵的博客中有什么地方不对也希望得到大家的指点,谢谢各位家人们的支持。你们的支持是小赵创作的动力,加油。

如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持小赵,如有不足还请指点,小赵及时改正,感谢大家支持!!!

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

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

相关文章

常用语音识别开源四大工具:Kaldi,PaddleSpeech,WeNet,EspNet

无论是基于成本效益还是社区支持,我都坚决认为开源才是推动一切应用的动力源泉。下面推荐语音识别开源工具:Kaldi,Paddle,WeNet,EspNet。 1、最成熟的Kaldi 一个广受欢迎的开源语音识别工具,由Daniel Pove…

Servlet框架

简介 Servlet是运行在web服务器或应用服务器上的程序,他是作为来自web浏览器或其他http客户端的请求和HTTP服务器上的数据库或应用程序之间的中间层。 使用Servlet可以手机来自网页表单的用户输入,呈现来自数据库或者其他源记录,还可以动态创…

IDEA访问不到静态资源

背景 我在resources下创建static文件夹,再创建front文件夹放前端资源,里面有index.html,游览器输入localhost:8011/front没反应。(resources/static/front/index.html) 解决办法 重启idea,清楚idea缓存&am…

设计模式之服务定位器模式

想象一下,你的Java应用是一座庞大的迷宫,里面藏着无数宝贵的服务宝藏,而你正需要一张精确的藏宝图来指引方向,迅速找到并利用这些宝藏。服务定位器模式,正是这样一张神奇的地图,它帮你动态定位并获取应用中…

stl容器 string类的基本操作

目录 一.string类的构造 二.string类的输出 1.传统字符串输出 2.通过迭代器进行输出 ​编辑 3.C11标准的范围for输出加auto推导类型 三.string类的各种迭代器 begin()和end() 利用迭代器遍历输出 利用迭代器修改字符串的字符 rbgin()和rend() 利用迭代器遍…

[论文阅读]Adversarial Autoencoders(aae)和代码

In this paper, we propose the “adversarial autoencoder” (AAE), which is a probabilistic autoencoder that uses the recently proposed generative adversarial networks (GAN) to perform variational inference by matching the aggregated posterior of the hidden …

【人工智能基础】RNN实验

一、RNN特性 权重共享 wordi weight bais 持久记忆单元 wordi weightword baisword hi weighth baish 二、公式化表达 ht</sub f(ht - 1, xt) ht tanh(Whhht - 1 Wxhxt) yt Whyht 三、RNN网络正弦波波形预测 环境准备 import numpy as np import torch …

服务器端优化-Redis内存划分和内存配置

6、服务器端优化-Redis内存划分和内存配置 当Redis内存不足时&#xff0c;可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕&#xff0c;并快速定位到内存占用的原因。 有关碎片问题分析 Redis底层分配并不是这个key有多大&…

PG 全页写

1.什么是全页写 修改一个块的时候&#xff0c;把块读到内存中&#xff0c;commit后,WAL写进程会触发写&#xff0c;把修改的块写到WAL日志文件&#xff0c;如果再往这个块中插入一条数据&#xff0c;数据缓冲区里面的块有两条数据了&#xff0c;再次commit后&#xff0c;PG会把…

图像处理--空域滤波增强(原理)

一、均值滤波 线性滤波算法&#xff0c;采用的主要是邻域平均法。基本思想是使用几个像素灰度的某种平均值来代替一个原来像素的灰度值。可以新建一个MN的窗口以为中心&#xff0c;这个窗口S就是的邻域。假设新的新的像素灰度值为&#xff0c;则计算公式为 1.1 简单平均法 就是…

在excel中,alt+13和alt+10都是什么字符?

1.回车符与换行符 Alt13是回车符&#xff0c;Alt10是换行符。 2.用在microsoft word中 在microsoft office中&#xff0c;回车符 和 换行符 对文本来讲都有换行的作用&#xff0c;但它们并不是同一种符号。下图是在word中两种字符的显示&#xff0c; 当使用 回车符 进行文本…

Ubuntu MATE系统下WPS显示错位

系统&#xff1a;Ubuntu MATE 22.04和24.04&#xff0c;在显示器设置200%放大的情况下&#xff0c;显示错位。 显示器配置&#xff1a; WPS显示错位&#xff1a; 这个问题当前没有找到好的解决方式。 因为4K显示屏设置4K分辨率&#xff0c;图标&#xff0c;字体太小&#xff…

TCP(TCP客户端、服务器如何通信)

一、TCP介绍 TCP的特点&#xff1a; 面向连接的协议&#xff1a;TCP是一种可靠的、面向连接的协议&#xff0c;在通信之前需要建立连接&#xff0c;以确保数据的可靠传输。这意味着在传输数据之前&#xff0c;发送方和接收方之间需要建立一条可靠的连接通道。流式协议&#x…

Spring Cloud架构进化实操:Eureka、Apollo、OpenFeign、Ribbon、Zuul组件

文章目录 前言一、引出二、服务注册与发现2.1 创建Eureka注册中心2.1.1 引入pom依赖2.1.2 配置yaml2.1.3 启动服务21.4 测试访问 2.2 创建服务提供者2.2.1 配置yaml2.2.2 启动服务2.2.3 测试访问 2.3 创建服务消费者2.3.1 服务提供者接口2.3.2 服务消费者调用接口 三、负载均衡…

Docker的私有仓库部署-Harbor

目录 一. Docker原生私有仓库 Registry 1. Registry 的介绍 2. Registry 的部署过程 二. Registry 的升级——Habor 1. Harbor 简介 2. Harbor 特性 3. Harbor 的构成 4. Harbor 部署 4.1 部署 Docker-Compose 服务 4.2 部署 Harbor 服务 4.2.1 下载或上传 Harbor…

18_Scala面向对象编程trait

文章目录 trait1.定义trait2.向类中混入特质2.1没有父类2.2有父类 3.动态混入3.1动态混入查询功能到公司业务中 4.父类&#xff0c;子类&#xff0c;特质初始化优先级5.Scala功能执行顺序6.常用API trait –特质的学习需要类比Java中的接口&#xff0c;源码编译之后就是interf…

三种方法解决:检测到在集成的托管管道模式下不适用的 ASP.NET 设置

几天前配置一个IIS环境的网站时,出现500错误。根据错误提示,很快把问题解决了,现记录一下,希望能帮到遇到同样问题的网友。 问题描述 (点击图片放大) 应用程序“DEFAULT WEB SITE”中的服务器错误Internet Information Services 7.5错误摘要 HTTP 错误 500.24 - Interna…

抓包证书安装到安卓7.0+手机

前言: 首先理解一下,这个不只是证书到浏览器,而是抓包证书到安卓7.0+手机上的文章; 还有一点区分,在浏览器上装的证书,只是让抓包工具可以抓取手机浏览器的包,而不是抓取手机app上的包; 如果你的证书只是简单的在浏览器下进行安装,那么你的手机app是走不了代理网络的…

视频教程下载:为 GPTs 商店构建 10 个 GPTs获得被动收入

欢迎来到 AI 驱动的内容创作新时代 - GPT 商店。这门综合课程是您成为定制和利用 GPT 模型解决多样化应用的专家的路线图。无论你是错过了应用商店革命的初始浪潮还是乘着它取得了成功&#xff0c;这都是你站在下一个重大数字飞跃前沿的机会。 课程模块&#xff1a; - 介绍 Ch…

Dragonfly 拓扑的路由算法

Dragonfly 拓扑的路由算法 1. Dragonfly 上的路由 (1)最小路由(2)非最小路由 2. 评估 Dragonfly 拓扑的路由算法 John Kim, William J. Dally 等人在 2008 年的 ISCA 中提出技术驱动、高度可扩展的 Dragonfly 拓扑。而文章中也提到了 针对 Dragonfly 拓扑的路由算法。本文对…