【数据结构初阶】之单链表

news2024/11/16 17:39:49

【数据结构初阶】之链表

  • 1. 链表是什么
    • 2. 单链表的逻辑结构和物理结构
  • 3.如何创建一个单链表的自定义类型
  • 4.单链表的增删查改及各种功能的实现
    • 4.1 单链表创建一个节点
    • 4.2 单链表的头插
      • 4.2.1 头插的函数设计(参数类型及其返回值)
      • 4.2.1 头插的函数实现
    • 4.3 单链表的头删
    • 4.4 单链表的尾插
    • 4.5 单链表的尾删
    • 4.6单链表的打印
    • 4.7单链表的在pos之前和之后插入数据
      • 4.7.1单链表在pos位置之前插入
      • 4.7.2单链表在pos位置之后插入
    • 4.8 单链表在pos位置和pos位置之后删除数据
      • 4.8.1 在pos位置删除数据
      • 4.8.2在pos位置之后删除数据
    • 4.9单链表查找数据
    • 4.10单链表销毁
  • 5.单链表各种功能的测试
    • 5.1 测试头插头删
    • 5.2 测试尾插尾删

❤️博客主页: 小镇敲码人
🍏 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌞任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞

1. 链表是什么

链表是数据结构里面一种常见的数据结构,主要用于数据的存储和管理,常见的功能是增删查改,依据是否有头节点、单向还是双向、循环还是不循环,通常有6种链表。

在这里插入图片描述
我们下面主要讲一下不带头不循环单链表,以下都简称单链表。

2. 单链表的逻辑结构和物理结构

在这里插入图片描述
在这里插入图片描述
从以上两幅图我们可以知道,单链表在物理结构上的存储其实是不连续的,而是因为它的当前节点中保存了下一个节点的地址,我们通过这个地址可以访问到下一个节点里面的内容,所以我们在分析问题的时候假想是连续的就像一个链子一样把节点串在一起。

3.如何创建一个单链表的自定义类型

那应该如何实现创建一个单链表的类型呢?我们在C语言里面知道是没有单链表这个类的,所以自定义类型一般使用struct创建一个结构体,结构体里面有两个成员,一个是数据域,一个是指针域(保存),那小伙伴可能就要问了,为什么我们不直接保存下一个节点,而是要保存它的地址呢?因为会出现如下问题:

typedef struct SListNode
{
	SLTDateType data;
	struct SListNode next;
}SListNode;

这样创建单链表的类型,正确与否先不论,你该如何计算这个结构体所占空间的大小呢,我们知道结构体有内存对齐的规则,那问题来了你的一个成员是同一个自定义类型的结构体对象,你如何知道它的大小呢?就出现了一系列的问题,但是如果使用结构体指针,指针的大小是固定的,就不会出现这种问题。

所以正确的创建单链表这个自定义类型的代码应该是这样的:

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

由于我们不清楚之后要存放的是什么类型的数据,所以使用typedef为我们的数据类型取个别名,之后的代码都将使用这个别名来代表我们单链表中存放的数据类型,因为如果之后数据类型发生变化只更改这一个地方就可以了。

4.单链表的增删查改及各种功能的实现

4.1 单链表创建一个节点

由于我们进行头插,或者尾插操作都需要创建新的节点,所以写一个创建节点的函数避免重复代码。

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if(newnode == NULL)
	{
			perror("malloc Failed\n");
			exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

为什么要使用malloc动态申请一个空间呢?因为我们动态开辟的空间是在堆上申请的,堆上申请的空间有一个特点,想要释放它的空间只有两种办法:1.手动free函数释放。2.main函数执行到return 0,也就是程序运行结束。也就是说我们在堆上开空间,即使出了这个函数的作用域,这片空间也不会被销毁,不然我们如果创建一个节点刚出这个函数还没和链表连接起来它的空间就被系统回收了,我们就无法成功创建链表了,除非你所有的代码只写在main函数里面,这显然是不现实的。

在这里插入图片描述
上图的代码是加一个判断,防止malloc申请空间失败,程序莫名其妙的挂了,而程序员不知道原因,这段代码可以增加代码的健壮性,perror是一个函数,它负责报错,我们在C语言字符串函数中有过较为详细的讲解,这里不再做过多的阐述,exit(-1)可以直接结束程序,与return 0的区别是后者只是退出某个函数,而前者是直接结束程序,并显示代码为-1,也就是下图箭头所指的地方:

在这里插入图片描述
这也是为了增加代码的健壮性。
动态申请一个节点时要对它进行初始化,包括两个部分,数据域和指针域,数据域好说,直接赋我们传过来的数据,指针域我们只需要给它赋值为NULL就行,防止出现野指针的问题,至于后续怎么处理,这个函数不负责。

4.2 单链表的头插

头插顾名思义,就是在链表的头节点之前插入一个新的节点。

4.2.1 头插的函数设计(参数类型及其返回值)

那应该如何设计这个函数呢?

  1. 参数类型
    首先我们想改变结构体的数据,函数传参应该如何设计呢?直接传结构体对象过去可以吗?显然是不行的,因为如果形参也是一个结构体类型的话,形参就只是它的一个拷贝,改变形参不会影响实参,所以我们首先想改变结构体里的数据,即在函数里给数据域赋值,可以影响到外面的结构体,就必须传结构体指针过去,它保存的是结构体的地址,改变它就是改变函数外面的结构体,但是这样就可以了吗?因为我们是不带头的单链表,如果头插遇见了刚好一个节点都没有的情况该怎么办呢?这个时候那个新开的节点就是我们的头节点,它的地址就是我们头节点的地址,所以我们改变原先头节点的地址,我们现在传的是结构体的地址,参数是一级指针,外面是结构体的地址也相当于是一级指针,我们想要在函数体里改变一级指针的值就必须用到二级指针,因为这两个一级指针除了保存的地址是一样的,其它都不一样,你在函数体里面改变这个一级指针,是无法影响到外面的一级指针的。
    至于返回值,头插不需要返回任何东西,所以类型是void.

4.2.1 头插的函数实现

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

因为要传结构体类型的二级指针我们在外面初始化结构体对象时,应该直接创建一个结构体指针,并将其初始化为NULL防止出现野指针的情况。

在这里插入图片描述

4.3 单链表的头删

有增加数据,自然就有删除数据,头删就是删除头节点。

既然删除了头节点,后面一个就是新的头节点,那我们就要改变头节点的地址,由于我们传的是头节点的地址,想改变它就还得用二级指针,但是头删补需要传数据域。
代码实现:

// 单链表头删
void SListPopFront(SListNode** pplist)
{
	//空
	assert(*pplist);

	//非空
	SListNode* newhead = (*pplist)->next;
	free(*pplist);
	*pplist = newhead;
}

assert函数可以强制判空,如果链表已经为空了,就不能继续删了如果没有这句话,我们就可能对NULL进行->操作,这是非法的,另外我们要先保存好下一个节点的地址,然后再去释放头节点的地址,如果不释放,程序只要不结束,这段空间就不会被系统回收,就会造成内存泄漏,顺序如果反了也不行,先释放头节点的空间,我们就无法找到头节点后面的节点了。

4.4 单链表的尾插

尾插也存在链表为空的情况,这个时候就需要改变头节点的地址,也需要用到结构体二级指针。

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

但是单链表没有保存尾节点的地址,所以需要我们自己遍历去找,一直到节点的next保存的是NULL为止,注意每次循环都要对tail指针进行更新,否则就成死循环了,找到后将尾节点的next指针指向新的节点就完成了尾插,注意空链表的情况要单独考虑。

4.5 单链表的尾删

尾删和头删对应,是删除尾节点,和尾插一样要循环找尾节点。

// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(*pplist);
	SListNode* tail = *pplist;
	SListNode* prevtail = *pplist;
	while (tail->next)
	{
		prevtail = tail;
		tail = tail->next;
	}
	if (prevtail->next)
		prevtail->next = NULL;
	else
		*pplist = NULL;
	free(tail);
}

assert函数帮助我们强制的规避链表为空的情况。当链表不为空时,我们遍历链表,保存尾节点和尾节点的前一个节点,因为我们需要将新的尾节点的next指针置为空,否则释放尾节点的空间之后,新的尾节点的next指针就变成野指针了,由于链表只存在一个节点时,尾节点和尾节点前面的一个节点相同,所以需要分情况讨论,区别就在于prevtail的下一个节点是否为空,只有只存在一个节点时才会为空,这个时候直接把头节点置为NULL,然后释放tail。

4.6单链表的打印

我们在写好单链表的各种功能后,经常要把单链表直观的打印出来以便我们查看功能是否正确,所以肯定会使用很多次打印,为避免代码重复,我们将其单独写成函数。由于我们有些地方要传二级指针,所以我们是直接创建的结构体指针,打印链表不存在更改头节点的地址,所以我们直接传一级指针就可以了。

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

4.7单链表的在pos之前和之后插入数据

4.7.1单链表在pos位置之前插入

在pos位置之前插入较为复杂,因为我们需要把节点链接到pos位置和pos位置之前的节点之间,就需要遍历链表寻找pos位置之前的节点。

// 在pos位置之前插入
void SLTInsert(SListNode** plist, SListNode* pos, SLTDateType x)
{
	assert(plist);
	assert(pos);
	if (pos == *plist)
		SListPushFront(plist, x);
	else
	{
		SListNode* newnode = BuySListNode(x);
		SListNode* prevpos = *plist;
		while (prevpos->next != pos)
		{
			prevpos = prevpos->next;
		}
		prevpos->next = newnode;
		newnode->next = pos;
	}
}

可以看到上述代码依然是分了两种情况,如果pos就是头节点,直接复用头插。另外一种情况就是要遍历找到pos位置的前一个节点,当前节点的下一个节点为pos时,就代表找到了,循环结束。

4.7.2单链表在pos位置之后插入

这种相对来说比较简单,我们先将新节点的next指针指向pos节点的next指针,然后在更新pos节点的指针域就插入完成了。

void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);
	SListNode* newnode = BuySListNode(x);
	SListNode* after = pos->next;
	pos->next = newnode;
	newnode->next = after;
}

如果采用上面那种方法先后顺序是不能变的,否则就无法将新节点和链表后面的节点连接在一起了,但是如果我们直接把pos后一个位置的节点指针用一个同类型的指针变量保存起来就不存在这种问题,无论你顺序怎么变,它都不会出错。

4.8 单链表在pos位置和pos位置之后删除数据

4.8.1 在pos位置删除数据

和在pos之前插入类似,我们需要循环找到pos位置之前的节点,让它和pos位置后一个节点链接起来。

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

依然分两种情况,如果pos节点和头节点相同,那么就复用头删。其它的情况则需要遍历找到pos位置前一个节点,然后先链接后释放空间。

4.8.2在pos位置之后删除数据

这种情况没有什么特殊的地方,就是注意对只有一个节点的情况进行处理。

void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);
	SListNode* after = pos->next->next;
	free(pos->next);
	pos->next = after;
}

4.9单链表查找数据

这个函数主要用来查找某个值所在的节点位置,并返回其节点地址,如果没找到就返回空指针,使用简单的遍历就可以实现。

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

4.10单链表销毁

单链表既然创建了,就会有销毁的函数,手动回收其在堆上开的空间,主要利用free函数和遍历实现,具体代码如下:

// 单链表的销毁
void SListDestroy(SListNode** plist)//传二级指针,因为后面需要将头节点的地址赋值为NULL
{
	assert(*plist);//如果链表已经为空,就不用继续销毁了。
	SListNode* cur = *plist;
	SListNode* after = (*plist)->next;
	while (cur)
	{
		free(cur);
		cur = after;
		if(after)
		after = after->next;
	}
	*plist = NULL;
}
  • 注意:*->,->的优先级更高,所以我们在解引用结构体二级指针时应该加上小括号,让其先与*结合,否则就会报错。

5.单链表各种功能的测试

5.1 测试头插头删

#include"slist.h"


//单链表测试--头插头删
void SListNodeTest1()
{
	SListNode* SL = NULL;
	SListPushFront(&SL, 2);
	SListPrint(SL);
	SListPushFront(&SL, 3);
	SListPrint(SL);
	SListPushFront(&SL, 4);
	SListPrint(SL);
	SListPushFront(&SL, 5);
	SListPrint(SL);
	SListPushFront(&SL, 6);
	SListPrint(SL);
	SListPushFront(&SL, 7);
	SListPrint(SL);
	SListNode* pos = SListFind(SL, 3);
	SListInsertAfter(pos, 100);
	SListPrint(SL);
	SLTInsert(&SL, pos, 200);
	SListPrint(SL);
	pos = SListFind(SL, 100);
	SListEraseAfter(pos);
	SListPrint(SL);
	SLTErase(&SL, pos);
	SListPrint(SL);
	SListPopFront(&SL);
	SListPrint(SL);
	SListPopFront(&SL);
	SListPrint(SL);
	SListPopFront(&SL);
	SListPrint(SL);
	SListPopFront(&SL);
	SListPrint(SL);
	SListPopFront(&SL);
	SListPrint(SL);
	SListPopFront(&SL);
	SListPrint(SL);
}
int main()
{
	SListNodeTest1();
	return 0;
}

运行截图:

在这里插入图片描述

5.2 测试尾插尾删

#include"slist.h"

//测试尾插尾删
void SListNodeTest2()
{
	SListNode* SL = NULL;
	SListPushBack(&SL, 2);
	SListPrint(SL);
	SListPushBack(&SL, 3);
	SListPrint(SL);
	SListPushBack(&SL, 4);
	SListPrint(SL);
	SListPushBack(&SL, 5);
	SListPrint(SL);
	SListPushBack(&SL, 6);
	SListPrint(SL);
	SListPushBack(&SL, 7);
	SListNode* pos = SListFind(SL, 3);
	SListInsertAfter(pos, 100);
	SListPrint(SL);
	SLTInsert(&SL, pos, 200);
	SListPrint(SL);
	pos = SListFind(SL, 100);
	SListEraseAfter(pos);
	SListPrint(SL);
	SLTErase(&SL, pos);
	SListPrint(SL);
	SListPopBack(&SL);
	SListPrint(SL);
	SListPopBack(&SL);
	SListPrint(SL);
	SListDestroy(&SL);
	SListPrint(SL);
}
int main()
{
	//SListNodeTest1();
	SListNodeTest2();
	
	return 0;
}

运行结果:

在这里插入图片描述

  • 注意:这里所有的功能都已经测试完毕,一切正常,我们在学习单链表时,要有建立工程的意识,函数声明及自定义类型的创建放在头文件,函数的定义放在一个.c文件,测试放在另一个.c文件,这样不仅仅有利于我们后续的复习,而且能使工程更加的健壮。

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

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

相关文章

【SOC基础】单片机学习案例汇总 Part1:电机驱动、点亮LED

📢:如果你也对机器人、人工智能感兴趣,看来我们志同道合✨ 📢:不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 📢:文章若有幸对你有帮助,可点赞 👍…

mybatisplus 增删改查

文章目录 1.BaseMapperBaseMapper中提供的CRUD方法: CURD实例通用ServiceIService中的CRUD方法 Service层操作数据实例 1.BaseMapper 说明: 通用 CRUD 封装BaseMapper 接口,为 Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器…

集成测试、单元测试、系统测试之间的关系和区别

前言 为了使软件正常工作,所有单元都应集成在一起并正常运行。集成测试就像是要求不同工种的工人修建一个房子,希望他们都团结协作。如何判断他们在一起是否可以按照计划完成建设呢?唯一了解的方法是通过将它们全部拉在一起并测试它们如何相互…

写时拷贝+进程终止

目录 一、写时拷贝 二、创建多进程 三、进程终止 (一)main函数的返回值 1. main函数的返回值 2. 退出码 3. 查看进程的退出码 (二)错误码VS退出码 (三)代码异常终止 1. 基本概念 2. 信号 &…

Visual Studio 2022打不开net4.5项目的解决方案

1 问题 是因为你本机没有装相关的netframework的包,具体文件夹的路径在: C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework 如下图: 进入目录看,我的net4.5.2是空的,如下图&#xf…

jsp 网上订餐Myeclipse开发mysql数据库web结构java编程计算机网页项目

选题目的 本设计课题为网上订餐系统设计与实现,提供无纸化点餐、支付,实现点餐快捷,方便管理。餐厅电子化是目前的潮流和趋势,大多数企业都将公司内部运营流程电子化。网上订餐管理系统应运而生,能够有效提高公司的管…

【干货】<img/>加载成功||失败

功能描述 img标签绑定URL时&#xff0c;图片加载会出现如下情况&#xff1a; 加载成功展示url加载失败显示默认图片&#xff08;require(/assets/images/imgError.png)&#xff09; dom代码 <img :src"url" alt"" error"errorImageShow($even…

第57篇-某钩招聘网站加密参数分析【2023-10-31】

声明:该专栏涉及的所有案例均为学习使用,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!如有侵权,请私信联系本人删帖! 文章目录 一、前言二、网站分析1.X-S-HEADER参数2.请求参数data3.响应机密值data一、前言 网址: aHR0cHM6Ly93d3cubGFnb3UuY29t…

优化 FPGA HLS 设计

优化 FPGA HLS 设计 用工具用 C 生成 RTL 的代码基本不可读。以下是如何在不更改任何 RTL 的情况下提高设计性能。 介绍 高级设计能够以简洁的方式捕获设计&#xff0c;从而减少错误并更容易调试。然而&#xff0c;经常出现的问题是性能权衡。在高度复杂的 FPGA 设计中实现高性…

【element-ui】表格

效果展示 组件代码 <el-table class"compTableClass" ref"tableOOOOO":class"(className in tableConfig)?tableConfig.className:":data"tableConfig.data" :height"tableConfig.height" style"width: 100%"…

【ES专题】ElasticSearch 高级查询语法Query DSL实战

目录 前言阅读对象阅读导航前置知识数据准备笔记正文一、ES高级查询Query DSL1.1 基本介绍1.2 简单查询之——match-all&#xff08;匹配所有&#xff09;1.2.1 返回源数据_source1.2.2 返回指定条数size1.2.3 分页查询from&size1.2.4 指定字段排序sort 1.3 简单查询之——…

CUMT-----Java课后第三章编程作业

文章目录 一、题11.1 题目描述1.2 代码块1.3 运行截图 二、题22.1 题目描述2.2 代码块2.3 运行截图 一、题1 1.1 题目描述 1.2 代码块 public class Book {private String name;private int pages;public String getname(){return name;}public void setname(String name){th…

HarmonyOS(二)—— 初识ArkTS开发语言(上)之TypeScript入门

前言 Mozilla创造了JS&#xff0c;Microsoft创建了TS&#xff0c;而Huawei进一步推出了ArkTS。因此在学习使用ArkTS前&#xff0c;需要掌握基本的TS开发技能。 ArkTS介绍 ArkTS是HarmonyOS优选的主力应用开发语言。它在TypeScript&#xff08;简称TS&#xff09;的基础上&am…

网络安全安全领域最容易搞混的四个名词:Network网络安全、Cyber网络安全、数据安全、信息安全

在安全领域我们经常会遇到Network网络安全、Cyber网络安全、数据安全、信息安全&#xff0c;它们看起来都差不多&#xff0c;那么它们之间有什么区别呢&#xff1f; 如果你对网络安全入门感兴趣&#xff0c;那么你需要的话可以点击这里&#x1f449;【入门&进阶全套282G学…

基于PyTorch的共享单车使用数量预测研究

收藏和点赞&#xff0c;您的关注是我创作的动力 文章目录 概要 一、预测模型的实现3.1数据的获取和预处理3.2划分数据集3.3构建神经网络 二、PyTorch框架三 原理2.1前馈神经网络2.1.1 BP神经网络 四 预测效果验证4.1小批量梯度下降4.2批量梯度下降4.3随机梯度下降 五 结 论目录…

javascript的webstorage数据存储问题,不能直接存undefined

这篇文章分享一下自己使用sessionStorage遇到的一个小问题&#xff0c;以后遇到要避坑。 需求是easyui表格的单元格编辑&#xff0c;点击保存的时候会结束当前行的编辑&#xff0c;然后修改editingId&#xff08;当前编辑行记录的ID&#xff09;。 目录 一、待解决问题 二、完…

CN考研真题知识点二轮归纳(3)

持续更新&#xff0c;上期目录&#xff1a; CN考研真题知识点二轮归纳&#xff08;2&#xff09;https://blog.csdn.net/jsl123x/article/details/134111760?spm1001.2014.3001.5501 1.TCP/IP 名称&#xff1a;传输控制协议/网络协议&#xff0c;是一个协议族&#xff0c;主…

机器人仿真——gazebo学习笔记(0)

Gazebo是一个功能强大的三维物理仿真平台&#xff0c;具有强大的物理引擎、高质量的图形渲染能力、重点是他是开源的、免费的。 1.Gazebo具备以下几个特点: 1.动力学仿真:支持多种高性能物理引擎,像ODE、Bullet等. 2.三维可视化环境:x、y、z三维环境。 3.传感器仿真:支持传…

【MATLAB】全网唯一的13种信号分解+FFT傅里叶频谱变换联合算法全家桶

有意向获取代码&#xff0c;请转文末观看代码获取方式~ 大家吃一顿火锅的价格便可以拥有13种信号分解FFT傅里叶频谱变换联合算法&#xff0c;绝对不亏&#xff0c;知识付费是现今时代的趋势&#xff0c;而且都是我精心制作的教程&#xff0c;有问题可随时反馈~也可单独获取某一…

数据仓库-拉链表

在数据仓库中制作拉链表&#xff0c;可以按照以下步骤进行&#xff1a; 确定需求&#xff1a;首先明确需要使用拉链表的场景和需求。例如&#xff0c;可能需要记录历史数据的变化&#xff0c;以便进行时间序列分析等。设计表结构&#xff1a;在数据仓库中&#xff0c;拉链表通…