【数据结构——单链表】本篇文章通过图文结合的方式能让你轻松的掌握单链表

news2024/11/24 0:31:50

链表的概念及结构

有了顺序表为什么还会出现链表呢?
链表和数组类似,但是功能比数组强大的多,数组的空间是固定的,在定义数组的时候空间大小就已经固定了,在使用时有可能会造成空间的浪费或者面临空间不够的风险,而链表的空间时动态的,则避免了这一问题。
概念
链表是一种物理上存储结构非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
线性表中的数据结点在内存中的位置是任意的,即逻辑上相邻的数据元素在物理位置(内存存储的位置)上不一定相邻。
链式存储结构的有优点

  • 空间利用率高需要一个空间就分配一个空间
  • 数据元素的逻辑次序靠节点的指针来指示,插入和删除时不需要移动数据结点,任意位置插入和删除时间复杂度为O(1)
    链式存储结构的缺点
  • 存储密度小,每个节点的指针域需要额外占用存储空间。当每个节点的数据域所占字节不多时,指针域所占空间比重显得很大,存储密度大空间利用率越大。
  • 链式存储结构时非随机存取结构,对任一节点的操作都要从头指针依次查找到该节点,算法复杂度比较高。
    链式存储的逻辑结构

从上图可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续,现实中的节点一般都是从堆上申请出来的。从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。

链表的分类

实际中的链表的结构非常多样:
1.单向或者双向
在这里插入图片描述

上图就是单向和双向循环的逻辑图
2.带头或不带头
在这里插入图片描述

上图就是带头和不带头的逻辑图
在这里插入图片描述

上图就是循环和非循环的逻辑图
链表的基本组合:
在这里插入图片描述

  • 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接等等。
  • 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是双向带头循环链表。另外这个结构虽然复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

链表的构成

链表是由一个个节点构成,每个节点一般采用结构体的形式组织,如下:

typedef int SLDataType;
typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SListNode;

链表节点分为两个域
数据域:存放各种类型的实际数据。
指针域:存放下一节点的首地址。

链表的操作

链表最大的作用是通过节点把离散的数据链接在一起,组成一个表。链表有那些常规操作呢?有如下操作:尾插、头插、尾删、头删、查找、在pos位置之后插入、删除pos位置之后的值等操作。
下面我们就来慢慢的分析:

动态申请空间:

首先是让链表满的时候动态申请空间,这样就不需要我们自己去手动的管理了。
1.使用malloc来创建新的节点
2.在判断节点是否创建成功
3.在给节点赋值,并把节点中的指针置空
4.返回节点的指针
代码如下:

//动态的申请节点
SListNode* BuySListNode(SLDataType x)
{
	
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode==NULL)
	{
		perror("malloc");
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

在我们检验链表功能的时候,我们需要打印到屏幕上才能清楚我们写的链表功能是否成功。

单链表的打印:

1.首先判断指针是否是空指针
2.创建一个新的指针来指向结构体,目的就是使用这个指针来遍历
注意:一点不要使用头指针来遍历,这样会导致我们丢失数据的。
代码如下:

//链表的打印
void SListPrint(SListNode* plist)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

下图是整个遍历的过程:
在这里插入图片描述

注意:这里有个坑就是循环结束的条件,一定是创建的指针走到空结束,而不是指针指向的next为结束条件

单链表的尾插:

首先我们要分情况,第一种就是传递过来的指针是空指针,第二种情况就是不是空指针的情况,着两种情况我们都要分别写代码。
1.首先我们新创建一个节点用来存储数据
2.在判断传递过来的指针是否是空指针
1)如果是空指针,那么我们直接返回新的节点
2)要是不是空指针,那么我们就创建一个新的指针来寻找尾节点
代码如下:

//单链表尾插
void SListPushBack(SListNode** pplist, SLDataType x)
{
	assert(pplist);
	SListNode* newnode = BuySListNode(x);
	if (*pplist==NULL)
	{
		*pplist = newnode;
	}
	else
	{
		SListNode* tail = *pplist;
		while (tail->next!=NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

下图是逻辑图:
在这里插入图片描述

注意:在寻找尾节点的时候要注意当next为空的时候那个节点就是位节点,所以我们使用tail->next来判断。

单链表的头插:

首先我们也要分三种情况:
第一种情况就是直接传递过来的指针是空指针,对于这种情况我直接使用断言来终止程序,
第二种情况就是传递过来的指针里面的内容是空,这种情况直接返回新的节点,
第三种情况就是我们传递过来的指针有指向的数据,那么我们直接插入节点就好了。
代码如下:
1.断言接收到的指针是否位空指针
2.创建一个新的节点,用来存储要插入的数据
3.要是接收到的指针内容为空那么直接返回新的节点
4.要是里面有链表那么直接插入

//单链表的头插
void SListPushFront(SListNode** pplist, SLDataType x)
{
	assert(pplist);
	SListNode* newnode = BuySListNode(x);
	if (*pplist==NULL)
	{
		*pplist = newnode;
	}
	else
	{
		
		newnode->next = *pplist;
		*pplist = newnode;
	}
}

下图是头插的逻辑图:
在这里插入图片描述

注意: 在第三步的时候不要把
newnode->next = *pplist;
*pplist = newnode;
这两行代码写反了如果写反了会导致后面的数据丢失。

单链表的尾删:

要考虑的情况:
1.是否接收的指针是空指针
2.是否只有一个节点
3.多个节点
要是为空指针那么直接就终止程序,要是只有一个节点直接释放当前节点,并且把它的头节点置空,要是有多个节点的情况我们就需要找到最后一个节点和倒数第二个节点,我们在释放最后一个节点的时候,也要把倒数第二个节点置空,只有这样才能不导致倒数第二个指针变为也指针。

void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	SListNode* prev = NULL;
	SListNode* tail = *pplist;
	// 1.空、只有一个节点
	// 2.两个及以上的节点
	if (tail == NULL || tail->next == NULL)
	{
		free(tail);
		*pplist = NULL;
	}
	else
	{
		while (tail->next)
		{
			prev = tail;
			tail = tail->next;
		}

		free(tail);
		tail = NULL;

		prev->next = NULL;
	}
}

下图是单链表的逻辑图:
在这里插入图片描述

注意:在第二种情况下一定要找到倒数第二个指针,不然容易造成野指针的错误。

单链表的头删:

要考虑的情况:
第一种:为空的情况
第二种:就是不为空的情况
代码如下:

//单链表的头删
void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	//为空的情况
	assert(*pplist);
	//不为空的情况
	SListNode* newnode = (*pplist)->next;
	free(*pplist);
	*pplist = newnode;
}

下图是头删的逻辑图:
在这里插入图片描述

第一种情况就是为空的,对于这种情况我们直接断言终止程序;
第二种情况不为空的,对于这种情况我创建一个新的节点来保存 * pplist 指向的位置,然后再释放 * pplist
,最后再把第二个节点设置为头节点。

单链表查找:

要考虑的情况:
第一种:为空的情况
第二种:遍历完了也没有找到
第三种:找到了返回当前指针
代码如下:

//单链表查找
SListNode* SListFind(SListNode* plist, SLDataType x)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data==x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

下图就是单链表的查找逻辑图:
在这里插入图片描述

对于为空的情况我直接就是使用断言来解决,要是找到了就返回当前节点的地址,要是没有找到就返回空。

单链表在pos之前插入数据:

要考虑的情况:
第一种:为空的情况
第二种:当pos的位置等于pplist的时候
第二种:就是正常的插入
代码如下:

// 在pos之前插入x
void SLTInsert(SListNode** pplist, SListNode* pos, SLDataType x)
{
	assert(pplist);
	assert(pos);
	if (pos==*pplist)
	{
		//直接调用头插
		SListPushFront(pplist, x);
	}
	else
	{
		//创建新的指针来指向头
		SListNode* cur = *pplist;
		//创建新的节点来存储数据
		SListNode* newnode = BuySListNode(x);
		//当cur->next不等于pos的时候就继续循环
		while (cur->next!=pos)
		{
			cur = cur->next;
		}
		cur->next = newnode;
		newnode->next = pos;
	}
}

下图是单链表在pos之前插入数据的逻辑图:
在这里插入图片描述

首先我使用的断言来判断指针是否为空,然后使用if来判断pos的位置是否等于pplist的位置,最后就是直接插入节点。

在pos位置之后插入数据:

这个比较简单,不需要考虑头尾的问题,只需要考虑,pos位置是否为空指针。
代码如下:

// 在pos以后插入x
void SLTInsertAfter(SListNode* pos, SLDataType x)
{
	assert(pos);
	//创建一个新的节点
	SListNode* newnode = BuySListNode(x);
	//当在中间插入的时候就需要这个步骤
	newnode->next=pos->next;
	pos->next = newnode;
}

下图是在pos位置之后插入数据的逻辑图:
在这里插入图片描述

删除pos位置的值:

第一种情况:当pos位置指向的是头的时候就直接调用头删
第二种情况:在尾和中间的时候,我们之间按照中间的处理方式处理就好了,因为在尾不需要特别处理。
代码如下:

void SLTErase(SListNode** pplist, SListNode* pos)
{
	assert(pplist);
	assert(pos);
	if (pos==*pplist)
	{
		SListPopFront(pplist);
	}
	else
	{
		SListNode* pre = *pplist;
		while (pre->next!=pos)
		{
			pre = pre->next;
		}
		pre->next = pos->next;
		free(pos);
	}
}

删除pos位置的数据的逻辑图:
在这里插入图片描述

单链表的销毁:

使用遍历的方式进行处理,边遍历边删除
代码如下:

 // 单链表的销毁
void SListDestroy(SListNode* plist)
{
	assert(plist);
	SListNode* del = plist;
	while (plist)
	{
		plist = del;
		del = del->next;
		free(plist);
	}
}

总代码

#define _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

//动态的申请节点
SListNode* BuySListNode(SLDataType x)
{
	
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode==NULL)
	{
		perror("malloc");
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

//链表的打印
void SListPrint(SListNode* plist)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

//单链表尾插
void SListPushBack(SListNode** pplist, SLDataType x)
{
	assert(pplist);
	SListNode* newnode = BuySListNode(x);
	if (*pplist==NULL)
	{
		*pplist = newnode;
	}
	else
	{
		SListNode* tail = *pplist;
		while (tail->next!=NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

//单链表的头插
void SListPushFront(SListNode** pplist, SLDataType x)
{
	assert(pplist);
	assert(pplist);
	SListNode* newnode = BuySListNode(x);
	if (*pplist==NULL)
	{
		*pplist = newnode;
	}
	else
	{
		
		newnode->next = *pplist;
		*pplist = newnode;
	}
}
//单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);
	if ((*pplist)->next==NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* tail = *pplist;
		//tail直接向后走两步这样可以避免使用第二个指针
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

//单链表的头删
void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	//为空的情况
	assert(*pplist);
	//不为空的情况
	SListNode* newnode = (*pplist)->next;
	free(*pplist);
	*pplist = newnode;
}

//单链表查找
SListNode* SListFind(SListNode* plist, SLDataType x)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data==x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

// 在pos之前插入x
void SLTInsert(SListNode** pplist, SListNode* pos, SLDataType x)
{
	assert((*pplist) && pos);
	if (pos==*pplist)
	{
		SListPushFront(pplist, x);
	}
	else
	{
		SListNode* cur = *pplist;
		SListNode* newnode = BuySListNode(x);
		while (cur->next!=pos)
		{
			cur = cur->next;
		}
		cur->next = newnode;
		newnode->next = pos;
	}
}

// 在pos以后插入x
void SLTInsertAfter(SListNode* pos, SLDataType x)
{
	assert(pos);
	//创建一个新的节点
	SListNode* newnode = BuySListNode(x);
	//当在中间插入的时候就需要这个步骤
	newnode->next = pos->next;
	pos->next = newnode;
}

//删除pos位置的值
void SLTErase(SListNode** pplist, SListNode* pos)
{
	assert(pplist);
	assert(pos);
	if (pos==*pplist)
	{
		SListPopFront(pplist);
	}
	else
	{
		SListNode* pre = *pplist;
		while (pre->next!=pos)
		{
			pre = pre->next;
		}
		pre->next = pos->next;
		free(pos);
	}
}

// 单链表的销毁
void SListDestroy(SListNode* plist)
{
	assert(plist);
	SListNode* del = plist;
	while (plist)
	{
		plist = del;
		del = del->next;
		free(plist);
	}
}

以上就是我关于数据结构中的单链表的细节问题和总结,下一篇博客我会写一篇关于单链表的力扣真题,并附上详细的讲解。

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

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

相关文章

2023年建筑架子工(建筑特殊工种)证考试题库及建筑架子工(建筑特殊工种)试题解析

题库来源:安全生产模拟考试一点通公众号小程序 2023年建筑架子工(建筑特殊工种)证考试题库及建筑架子工(建筑特殊工种)试题解析是安全生产模拟考试一点通结合(安监局)特种作业人员操作证考试大纲和(质检局)特种设备作…

第九章 内存分区模型

C程序在执行时,将内存大方向划分为4个区域 代码区:存放函数体的二进制代码,由操作系统进行管理的 全局区:存放全局变量和静态变量以及常量 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等 堆区&#xff1…

【NLP的Python库(04/4)】:Flair

一、说明 Flair是一个现代的NLP库。从文本处理到文档语义,支持所有核心 NLP 任务。Flair使用现代转换器神经网络模型来完成多项任务,并结合了其他Python库,可以选择特定的模型。其清晰的API和注释文本的数据结构,以及多语言支持&a…

FPGA project : uart232_ram_vga

重点学习: 本实验重点学习了双口ram解决多bit跨时钟域同步处理的问题。 其实signal port ram,它的输入口和输出口分别用不同的时钟,也可以解决这个问题。 让我意识到的比较重要的事情: 1,代码设计中,一…

VEX —— Functions|Measure

目录 distance —— 返回两点间距离 distance2 —— 返回两点间距离平方 surfacedist —— 查找点到点组的距离(沿几何体表面) xyzdist —— 查找点到表面最近点的距离 uvdist —— 在uv空间查找uv坐标到几何体的距离 planepointdistance —— 计算…

【免费】2023云栖大会门票开抢啦!数量有限,先到先得!

🎫 报名方式:点击链接即可免费报名! 🔗 2023云栖大会-领票页 📅 10月31日-11月2日,让我们齐聚云栖小镇!

基于 Nginx 实现一个灰度发布系统

软件开发一般不会上来就是最终版本,而是会一个版本一个版本的迭代。新版本上线前都会经过测试,但就算这样,也不能保证上线了不出问题。所以,在公司里上线新版本代码一般都是通过灰度系统进行测试。并且,灰度系统可以把流量划分成多份,一份走新版本代码,一份走老版本代码…

数据通信——应用层(文件传输FTP)

引言 域名保证了在因特网中标识唯一的用户,而我们用户上网需求自然是发送信息以及共享文件,我们使用的很多传输工具,比如微信、QQ、百度等软件,在上传和下载文件时就会有FTP的参与。那么如何为文件提供收发渠道以完成的共享呢&…

一篇文章带你了解最近很火的RunnerGo测试平台

在当今这个数字化时代,应用程序的性能至关重要。一款可靠的性能测试工具,能够为企业带来无数的好处。最近,一款名为RunnerGo的开源性能测试工具备受瞩目。本文将详细介绍RunnerGo的特点、优势以及如何解决性能测试中的痛点。 RunnerGo产品介绍…

位运算符与高级操作

位运算符与高级操作 运算符 高级操作 左移实现乘法 左移n位等价于乘以2的n次方 int x; x 2; x x << 2; x x << 3;使用左移实现乘法运算仅限于乘以2的倍数 是不是只要左移就能够实现乘以2的倍数呢? char x 120; x x << 1;右移实现除法 右移n位等价于除…

ipad触控笔有必要买吗?比较好用的电容笔

如果你想用iPad画画&#xff0c;苹果Pencil会是个不错的选择。然而&#xff0c;苹果原装的电容笔价格却高得让很多人望而却步。所以&#xff0c;比较好的办法就是选用一个平替电容笔。我曾经使用过ipad&#xff0c;也是一个数码爱好者&#xff0c;最近两年我开始接触使用平替电…

Spring MVC 中的数据验证技术

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

cmdb运维管理平台在哪能看

cmdb运维管理平台功能你可以在云呐 进行查看  CMDB运维管理平台是一种IT资产管理工具&#xff0c;它可以帮助企业对IT资源进行有效的管理和监控。以下是一些常见的应用场景&#xff1a;  IT资产清单管理&#xff1a;通过CMDB运维管理平台可以对企业的IT资产进行全面的清单管…

微软考虑引入小型核反应堆;诺基亚推出“网络即代码”平台丨RTE开发者日报 Vol.58

开发者朋友们大家好&#xff1a; 这里是「RTE 开发者日报」&#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE &#xff08;Real Time Engagement&#xff09; 领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「…

CISSP,你值得拥有(我的学习之路)

&#xff08;只分享三点&#xff1a;怎么学、怎么练、怎么考。&#xff09; 我为啥去考CISSP 我是个在信安行业摸爬滚打将近20年的老油条&#xff0c;知道CISSP这个认证是很早前的事情了&#xff0c;但一直以来都觉得它有点难&#xff0c;加上人又懒得要命&#xff0c;也就始…

学会安装Redis数据库到服务器或计算机(Windows版)

Redis 是一个基于内存的开源数据库系统&#xff0c;被广泛应用于 Web 应用、消息队列、缓存、实时统计等领域。它支持多种数据结构&#xff0c;包括字符串、哈希表、列表、集合、有序集合等&#xff0c;并提供了多种操作命令。 Redis 的特点如下&#xff1a; 内存存储&#xf…

打开网站显示“不安全”怎么办?

在互联网世界中&#xff0c;安全是一个至关重要的问题。然而&#xff0c;当您尝试访问某些网站时&#xff0c;可能会看到“不安全”的警告。这通常是因为这些网站没有部署SSL证书。SSL证书是一种数字证书&#xff0c;可确保互联网通信的安全性和保密性。 “打开网站显示不安全”…

什么是城市坐标系,与国家坐标系的区别?

文章目录 先说国家坐标系什么是城市坐标系城市坐标系建设规范常见的城市坐标系 先说国家坐标系 先1954年我国建立了第一代国家大地坐标系统&#xff0c;即北京54坐标系&#xff0c;英文缩写BJ54&#xff0c;坐标原点在苏联&#xff0c;椭球参数直接拿苏联的。第一代坐标系的椭…

八大排序详解

目录 1.排序的概念及应用 1.1 排序的概念 1.2 排序的应用 1.3 常见的排序算法 2.常见排序算法的实现 2.1 直接插入排序 2.1.1 基本思想 2.1.2 动图解析 2.1.3 排序步骤&#xff08;默认升序&#xff09; 2.1.4 代码实现 2.1.5 特性总结 2.2 希尔排序 2.2.1 基本思…

基于Xilinx UltraScale+ MPSOC(ZU9EG/ZU15EG)的高性能PCIe数据预处理平台

PCIE707是一款基于PCIE总线架构的高性能数据预处理FMC载板&#xff0c;板卡具有1个FMC&#xff08;HPC&#xff09;接口&#xff0c;1路PCIe x4主机接口、1个RJ45千兆以太网口、2个QSFP 40G光纤接口。板卡采用Xilinx的高性能UltraScale MPSOC系列FPGA作为实时处理器&#xff0c…