数据结构入门指南:链表(新手避坑指南)

news2024/11/17 14:40:12

目录

前言

1.链表

1.1链表的概念

 1.2链表的分类

1.2.1单向或双向

1.2.2.带头或者不带头

1.2.33. 循环或者非循环

1.3链表的实现

 定义链表

总结


前言

        前边我们学习了顺序表,顺序表是数据结构中最简单的一种线性数据结构,今天我们来学习链表,难度相较于顺序表会大幅增加,非常考验大家对结构体、指针的理解。但是也不要害怕,我会一一向大家解答疑惑,本期的内容先给初学者预预热,主要介绍在刚开始学习链表时需要注意的点、涉及的基础知识以及逻辑基础,下期会将功能接口具体实现。


1.链表

1.1链表的概念

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

 逻辑图:

 而现实中的链表是这样的:

         通过图我们可以观察到,链式数据结构在逻辑上是连续的,在物理上不一定连续。链表的好处在于对内存更高效的使用(加入一个节点就开辟一个节点的空间)。

注意:

  • 链表的节点是在堆上开辟的,程序不结束就不会主动释放。
  • 从堆上申请的空间是按照一定策略分配的,两次申请的空间可能连续,也可能不连续。

 1.2链表的分类

        链表主要分为以下几种:

1.2.1单向或双向

         单向的链表简称为单链表,单链表只可以进行单向遍历,而双向链表完美的弥补了这个缺陷,可以向前遍历也可以向后遍历

1.2.2带头或者不带头

        它们本质上并没有太大的区别,在链表功能实现过程中,有头节点的不需要对特殊节点进行特殊操作,相对简单,但对于大部分的刷题网站上链表的题目都是默认为无头节点的,所以对于无头节点链表的理解更为重要。

1.2.3 循环或者非循环

 

        循环链表可以用于解决一些特定的问题,非循环链表一般用于普通的链表操作,例如插入、删除、查找等。

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

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

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

1.3链表的实现

         对于初学者来说,我个人建议先从无头单向非循环链表学起,因为在很多的刷题场景中都是单项无头非循环链表,理解了它也可以帮助你更快的适应链表的刷题。今天我们主要从这种单链表讲起。

         单链表的实现过程中会存在很多的坑,对于初学者来说是很困难的,我会一一列举帮助大家避免这些错误,刚开始我会从基础知识层面进行逐个分析,当然内容也会很多,我会分期进行讲解。

 定义链表

typedef int Datatype;
typedef struct SLNode
{
	Datatype data;
	SLNode* next;
}SLNode;

         定义链表这里就迎来了第一个坑,上述的这种形式很常见,对于初学者来说这里就有一个坑,这个链表定义的是否正确呢?

         这种方式是不正确的,为什么?typedef是重命名,结构体是我们自定义类型,SLNode是重命名后的名字,但是这里需要注意,这个重命名是从上述代码第六行结束后才开始生效,在结构体中提前使用是不允许的。

正确的定义:

typedef int Datatype;
typedef struct SLNode
{
	Datatype data;
	struct SLNode* next;
}SLNode;

         定义之后就需要创建节点,创建节点这里我们要明白:我们是通过函数的形式来创建节点,创建时顺便赋值,初学者或许会有这样的疑问:那为什么函数的类型是结构体指针类型?例如:

SLNode* NewNode(Datatype x)
{
}

        为了后续节点的链接,我们最后需要返回新建节点的地址,而节点地址的类型就是SLNode* ,函数的类型也只是函数的返回类型。

 接下来就是功能实现:

SLNode* NewNode(Datatype x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(Datatype));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

 这里就迎来了第二个坑, 这段代码哪里有问题?

        首先我们要清楚链表开辟空间是给谁开辟的,链表的空间是给整个节点(结构体)开辟的,而不是仅仅给数据开辟空间。

 所以说这里malloc的大小应该是结构体的大小,改为

SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));

就可以啦!

         在其他功能实现之前,我们先理解以下打印链表的函数接口。

void SLprint(SLNode* phead)
{
	SLNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

         打印链表首先需要遍历链表,phead为链表的头指针,但是一般情况下我们并不会直接的使用头指针去遍历,因为那样可能会造成头节点的丢失,所以我们需要创建另一个变量来接收头指针去执行遍历操作。我们要想让cur向后移动就只需把下一个节点的地址赋值给cur,我们知道一个链表的节点内会存储着下一个节点的地址,也就是当前节点中的next。注意这里的遍历需要好好的理解,这是进行后续理解的前提。

         理解完链表的的遍历,接下我们来说说它们是如何将每个新建的节点链接的。初学链表可能会有这样的疑惑:创建的节点是如何一个个链接起来的呢?节点的链接是属于后续头插,尾插等操作的内容,但是为了解答大家内心的疑惑,我们先写一个简单的测试接口,来演示它是如何链接的

void test1()
{
	SLNode* plist = NULL;//链表的头指针,没有节点的情况下指向NULL
	int n = 0;
	printf("请输入链表的长度\n");
	scanf("%d", &n);
	printf("请输入数据\n");
	for (int i = 0; i < n; i++)
	{
		int val = 0;
		scanf("%d", &val);
		SLNode *newnode= NewNode(val);//创建结构体指针变量来接收新节点的地址
        //头插的方式链接
		newnode->next = plist;
		plist = newnode;
	}
    SLprint(plist);
}



int main()
{
	test1();


	return 0;
}

         首先我们把头指针指向的内容(可能是NULL,也可能是第一个节点)赋值给新节点。

         初始情况下,plist指向NULL,把plist指向的内容赋值给新节点newnode的next(指针域),然后把整个新节点的地址赋给plist。

         这样plist就成功指向了第一个节点。那么后续插入增加节点也是这样。假设上一步链接的节点地址是0x0012ff40。

         把plist指向的节点(地址)赋值给新建节点newnode的next(结构体内的成员)。

         然后把整个新节点的地址赋给plist。这样就通过头插将节点链接了。但是这里需要注意,测试接口的操作是在同一个函数内进行创建头指针,节点链接操作的(不需要传值),没有调用函数进行头插链接(在后续实现头插,尾插接口的过程中需要使用二级指针传参进行操作)。

 接着我们继续,尾插操作的实现

        尾插的原理是什么?找到最后一节点。

 将新节点的地址赋给最后一个节点的next(指针域)就可以了。

 

 注意:新节点的next(指针域)在创建初始化时就已经置为NULL。

        但这并不完整,前边我们提到,尾插也可以用来初始化,那么对于没有节点的情况,上述逻辑就无法使用了。所以我们需要特殊处理一下,判断链表是否为NULL。

void SLPushBlack(SLNode* phead, Datatype x)
{
	SLNode* newnode = NewNode(x);
	SLNode* tail = *pphead;
	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		while (tail->next)//找到最后一个节点
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

         以上的逻辑可以这样实现,那段代码是否正确呢?这里就是遇到的第三个坑。运行测试一下我们会发现链表为空的时候没有插入数据。这是为什么呢?

        那是因为传进来的头节点是形参,形参是实参的临时拷贝,出了函数phead就被销毁了,在函数内将新节点地址赋给形参 头节点并不会对实参中的头节点造成影响。那这里要想修改实参中头节点的指向就只能传头节点的地址(二级指针),通过实参中头指针的地址来修改头指针的指向。

 所以正确的代码应该是:

void SLPushBlack(SLNode** pphead, Datatype x)
{
	SLNode* newnode = NewNode(x);
	SLNode* tail = *pphead;
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}
//调用时要传结构体指针的地址
SLPushBlack(&plist,100);

 那或许会有人疑惑,为什么链表不为的时候就不使用二级指针?

        这里我们总结一下,使用二级指针是因为要修改结构体指针(头指针)的指向,而链表不为空时,只需要链接增加一个节点就好,这时修改的是结构体成员的内容。

对头插操作进行实现

         使用二级指针是因为要修改结构体指针(头指针)的指向,那按照逻辑,头插每次都是修改头指针的指向,所以头插独立实现成一个函数时也需要二级指针,尾插是只有第一次链表为空的时候需要二级指针,而头插次次都需要二级指针。

        注意前边测试的接口头插数据没有使用二级指针是因为创建头指针和修改头指针指向在同一个函数中,不存在函数调用头指针。

前边我们已经了解头插的逻辑,这里就不再讲解。代码实现:

void SLPushFront(SLNode** pphead, Datatype x)
{
	SLNode* newnode = NewNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

         pphead就是指向plist(头指针)的指针,*pphead就等价于plist。通过对pphead解引用找到plist(头指针),直接修改plist(头指针)的指向。


总结

        本期内容主要介绍在刚开始学习链表时需要注意的点、涉及的基础知识以及逻辑基础,一定要理解透彻,否则后续的接口实现将会寸步难行,好的,本期内容到此就要结束啦,希望对你有所帮助,最后,感谢阅读!

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

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

相关文章

数据结构: 线性表(顺序表实现)

文章目录 1. 线性表的定义2. 线性表的顺序表示:顺序表2.1 概念及结构2.2 接口实现2.2.1 顺序表初始化 (SeqListInit)2.2.2 顺序表尾插 (SeqListPushBack)2.2.3 顺序表打印 (SeqListPrint)2.2.6 顺序表销毁 (SeqListDestroy)2.2.5 顺序表尾删 (SeqListPopBack)2.2.6 顺序表头插 …

vue中各种混淆用法汇总

✨在生成、导出、导入、使用 Vue 组件的时候&#xff0c;像我这种新手就会常常被位于不同文件的 new Vue() 、 export default{} 搞得晕头转向。本文对常见用法汇总区分 new Vue() &#x1f4a6;Vue()就是一个构造函数&#xff0c;new Vue()是创建一个 vue 实例。该实例是一个…

UE5 与 C++ 入门教程·第二课:动画重定向

虚幻中的角色动画都是基于 骨骼网格体 &#xff08;Skeletal Mesh&#xff09;实现&#xff0c;换言之&#xff0c;动画是跟骨骼网格体绑定的。如果有两个骨骼网格体&#xff0c;各自有一套角色动画&#xff0c;那么就可以通过重定向&#xff08;Retargeting&#xff09;将两个…

cglib动态代理、jdk动态代理及spring动态代理使用

1.项目初始化 1.1 pom.xml <dependencies><!-- spring依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.5.RELEASE</version></dependency>&l…

6个电池均衡,buckboost电路,精度高,均衡速度快,BMS均衡

6个电池均衡&#xff0c;buckboost电路&#xff0c;精度高&#xff0c;均衡速度快&#xff0c;本人原创。 1、主体电路图如下 2、均衡电压如图 3、平均电压波动图

SpringBoot使用PropertiesLauncher加载外部jar包

启用SpringBoot的PropertiesLauncher 使用SpringBoot的PropertiesLauncher可以优先加载外部的jar文件, 这样可以在程序运行前替换jar包, 官方文档: Launching Executable Jars 使用演示 建立一个SpringBoot工程, 工程中依赖一个叫自定义的utils包, 版本是1.0.0, 通过http接口…

Java基础_网络编程

Java基础_网络编程 网络编程三要素InetAddress网络模型 UDP通信程序单播发送数据接收数据聊天室 组播广播 TCPTCP通信程序三次握手和四次挥手 来源Gitee地址 网络编程三要素 IP: 设备在网络中的地址&#xff0c;是唯一的标识端口号: 应用程序在设备中唯一的标识。协议: 数据在…

2023OpenFeign源码

原理-源码 让我们看着源~码~ 按顺序走~趟流~程~ 分为两大部分&#xff1a;启动原理、调用流程 Feign 的Java 代码才 3w 多行&#xff0c;放眼现在热门的开源项目&#xff0c; Dubbo、Naocs、Skywalking 中 Java 代码都要 30w 行起步。 重要通知&#xff01;看源码&#xff0c;…

Winform制作的用户界面在高DPI下缩放问题

引言 熟悉Winform的小伙伴应该都遇到过 在100%缩放下制作的用户界面在其他缩放百分比下会出现字体超出边框的情况&#xff0c;导致用户体验大打折扣。用户程序DPI感知是默认打开的&#xff0c;此时可以通过关闭这种感知来禁用字体的缩放&#xff0c;在这种情况下&#xff0c;用…

C语言指针详解

目录 指针是什么? 指针和指针类型 指针-整数 指针的解引用 野指针 野指针成因 如何规避野指针 指针运算 指针- 整数 指针-指针 指针的关系运算 指针和数组 二级指针 指针数组 指针数组 模拟二维数组 指针是什么? 指针理解的2个要点: 1. 指针是内存中一个…

51单片机——串行口通信

目录 1、51单片机串口通信介绍 2、串行口相关寄存器 2.1 、串行口控制寄存器SCON和PCON 2.1.1 SCON&#xff1a;串行控制寄存器 (可位寻址) 2.1.2 PCON&#xff1a;电源控制寄存器&#xff08;不可位寻址&#xff09; 2.2、串行口数据缓冲寄存器SBUF 2.3、从机地址控制…

iOS - 解压ipa包中的Assert.car文件

项目在 Archive 打包后&#xff0c;生成ipa包 将 xxx.ipa文件修改为zip后缀即 xxx.zip &#xff0c;然后再双击解压&#xff0c;会生成一个 Payload 文件夹&#xff0c;里面一个文件 如下图&#xff1a; 然后显示改文件的包内容&#xff1a; 解压 Assets.car 文件的方式&…

基于x-scan的渲染算法

基于x-scan算法实现的z-buffer染色&#xff0c;.net core framework 3.1运行。 x-scan算法实现&#xff1a; public List<Vertex3> xscan() {List<Vertex3> results new List<Vertex3>();SurfaceFormula formula getFormula();Box rect getBound();for …

力扣 968. 监控二叉树

题目来源&#xff1a;https://leetcode.cn/problems/binary-tree-cameras/description/ C题解&#xff08;来源代码随想录&#xff09;&#xff1a;节点可以分为3个状态&#xff1a;0无覆盖&#xff1b;1有摄像头&#xff1b;2有覆盖。 要想放的摄像头最少&#xff0c;应当叶子…

无涯教程-jQuery - stop( clearQueue, gotoEnd)方法函数

stop([clearQueue&#xff0c;gotoEnd])方法停止所有指定元素上的所有当前正在运行的动画。 stop( [clearQueue, gotoEnd ]) - 语法 selector.stop( [clearQueue], [gotoEnd] ) ; 这是此方法使用的所有参数的描述- clearQueue - 这是可选的布尔参数。设置为true会清除动画…

绕过TLS/akamai指纹护盾

文章目录 前言TLS指纹什么是TLS指纹测试TLS指纹绕过TLS指纹使用原生urllib使用其他成熟库&#xff01;&#xff01;修改requests底层代码 Akamai指纹相关&#xff08;HTTP/2指纹&#xff09;什么是Akamai指纹测试Akamai指纹绕过Akamai指纹使用其他成熟库 实操参考 前言 有道是…

Eureka 学习笔记3:EurekaHttpClient

版本 awsVersion ‘1.11.277’ EurekaTransport 用于客户端和服务端之间进行通信&#xff0c;封装了以下接口的实现&#xff1a; ClosableResolver 接口实现TransportClientFactory 接口实现EurekaHttpClient 接口实现及其对应的 EurekaHttpClientFactory 接口实现 private …

【雕爷学编程】MicroPython动手做(16)——掌控板之图片图像显示2

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

第120天:免杀对抗-防朔源防流量防特征CDN节点SSL证书OSS存储上线

知识点 #知识点&#xff1a; 1、CS-CDN节点-防拉黑 2、CS-SSL证书-防特征 3、CS-OSS存储-防流量#章节点&#xff1a; 编译代码面-ShellCode-混淆 编译代码面-编辑执行器-编写 编译代码面-分离加载器-编写 程序文件面-特征码定位-修改 程序文件面-加壳花指令-资源 代码加载面-D…

springCloud Eureka注册中心配置详解

1、创建一个springBoot项目 2、在springBoot项目中添加SpringCloud依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2021.0.3</version><type>…