数据结构_单链表

news2025/3/12 8:56:03

在这里插入图片描述

今天我们要开启链表的学习

🖋️🖋️🖋️
学了顺序表我们可以知道:
🎈链表其实就是争对顺序表的缺点来设计的,补足的就是顺序表的缺点
🎈链表在物理上是上一个节点存放的下一个节点的地址

链表

1.链表的概念及结构

1.1概念

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

1.2结构
在这里插入图片描述
根据上图我们可以知道:
链表是由一系列节点组成的线性数据结构,每个节点包含数据部分和指向下一个节点的指针(在双向链表中还有指向前一个节点的指针)。节点通过指针连接在一起,形成一个链式结构,数据在内存中存储并不连续。

2.链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1.单向、双向
2.带头、不带头
3.循环、非循环
(这里图就不展示啦,大家感兴趣的可以自己去查找噢)

3.链表的实现

1.创建一个节点

typedef int SListDataType;
//节点
typedef struct SListNode
{
	//定义一个数据
	SListDataType data;//这个data就是下图中的1 2 3
	//定义一个指针
	struct SListNode* next;//next就是存放的指针
}SListNode;

这部分代码定义了一个单链表的节点结构:

  • struct SListNode:定义了一个名为 SListNode 的结构体,用于表示单链表中的一个节点。
  • SListDataType data;:在结构体中定义了一个成员变量 data,其数据类型为之前定义的 SListDataType(这里实际上就是 int 类型)。这个成员变量用于存储链表节点所包含的数据。
  • struct SListNode* next;:定义了一个指向struct SListNode类型的指针 next。这个指针用于指向下一个链表节点,通过它可以将多个节点连接起来形成链表。

2.遍历打印链表

void SListPrint(SListNode* phead)
{
	//这里不需要assert断言,因为这里为空的话就是空链表
	SListNode* cur = phead;//指向第一个节点
	while (cur != NULL)
	{
		printf("%d ", cur->data);
		cur = cur->next; 
	}
}

画图解释:
在这里插入图片描述

3.尾插

//动态申请新节点并初始化
SListNode* BuySListNode(SListDataType x)
{
	SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
	//在内存的堆区动态分配一块连续的内存空间,大小为 SListNode 结构体的字节数,再强转为SListNode*型
	if (newNode == NULL)
	{
		printf("申请结点失败\n");
		exit(-1);
	}
	newNode->data = x;//将传入的数据 x 存储到新节点的 data 成员
	newNode->next = NULL;//初始化新节点的 next 指针为 NULL(新节点暂时无后续节点)

	return newNode;//返回创建好的节点地址
}


//尾插
void SListPushBack(SListNode** pphead, SListDataType x)
{
	SListNode* newNode = BuySListNode(x);//调用函数创建待插入的新节点
	if (*pphead == NULL)
	{
		*pphead = newNode;//若链表为空,直接让头指针指向新节点
	}
	else
	{
		//找尾
		SListNode* tail = *pphead;//定义指针 tail 指向链表头部
		while (tail->next != NULL)//遍历链表,找到尾节点(尾节点的 next 为 NULL)
		{
			tail = tail->next;//实现遍历链表的关键代码,将指针 tail 移动到下一个节点
		}
		
		tail->next = newNode;//将尾节点的 next 指针指向新节点,完成尾插
	}
	
}

问题:动态申请return newNode;为什么要返回创建好的节点地址

返回新节点的地址是为了让调用者能够:

  1. 节点插入链表中。
  2. 后续操作节点的数据或指针。
  3. 正确释放内存(避免泄漏)。

这是 C 语言中动态内存管理的标准做法,确保函数间的数据传递和内存控制权的转移。

4.尾删

//尾删
void SListPopBack(SListNode** pphead)
{
	//1.空
	//2.一个结点
	//3.一个以上结点
	
	// 处理空链表
	if (*pphead == NULL)
	{
		return;//直接返回,没得删
	}
	
    // 处理单节点链表
	else if ((*pphead)->next ==NULL)
	{
		free(*pphead);
		*pphead = NULL;//只有这一个就直接删
	}
	
    // 处理多节点链表
	else
	{
		SListNode* prev = NULL;//用于记录尾节点的前驱节点
		SListNode* tail = *pphead;//用于遍历链表
		while (tail->next  != NULL)
		{
			prev = tail;//在循环中,prev 始终指向 tail 的前一个节点,tail 逐步后移
			tail = tail->next;//再往后走
		}
		free(tail);
		tail = NULL;
		prev->next = NULL;//将其存放的地址置空,避免内存泄漏和悬空指针
	}
}

5.头插

//头插
void SListPushFront(SListNode** pphead, SListDataType x)
{
	SListNode* newnode = BuySListNode(x);//创建新节点
	newnode->next = *pphead;//新节点的 next 指向原头节点
	*pphead = newnode;//更新头指针为新节点
}

步骤分析:

  • 步骤 1:创建新节点
    调用 BuySListNode(x)
    动态分配内存,初始化data xnext NULL
  • 步骤 2:连接新节点与原头节点
    newnode->next = *pphead;
    若原链表非空,新节点的next指向原头节点(形成链式关系)。
    若原链表为(*pphead == NULL),新节点的next仍为 NULL
  • 步骤 3:更新头指针
    *pphead = newnode;
    将头指针pphead指向新节点,使其成为链表的新头部。

画图解释:
在这里插入图片描述
注意:

  1. 核心原因:头指针是链表的入口,必须指向第一个节点。头插法中,新节点成为第一个节点,因此必须更新头指针。
  2. 技术实现:通过双重指针修改原始头指针,确保链表结构正确。若省略此步骤,新节点将无法被链表访问,导致内存泄漏或逻辑错误。

6.头删

//头删
void SListPopFront(SListNode** pphead)
{
	//1.空
	//2.一个节点 + 3.一个以上节点

	if (*pphead == NULL)
	{
		return;
	}
	else
	{
		SListNode* next = (*pphead)->next;//保存头节点的下一个节点 
		free(*pphead);// 释放头节点内存
		*pphead = next;// 更新头指针为原头节点的下一个节点  
	}
}

详细分析:

  • 保存后续节点:SListNode* next = (*pphead)->next; 记录原头节点下一个节点,防止链表断裂。
  • 释放内存:free(*pphead); 释放原头节点的内存,避免内存泄漏。
  • 更新头指针:*pphead = next; 使头指针指向原头节点的下一个节点,完成头删。

举个例子:
在这里插入图片描述
注意

  1. 左边 next:是程序员自定义的指针变量,用于临时存储地址。
  2. 右边 next:是链表节点结构体固有的成员,用于维系链表的链式结构。

二者仅名称相同,但一个是变量标识,一个是结构体成员,功能和性质完全不同。

7.单链表查找

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

详细分析:

  • SListNode* cur = phead;定义一个指针cur并初始化为 phead,用于遍历单链表。cur 会从链表的头节点开始,依次向后移动;
  • 在循环内部,使用if语句检查当前节点cur的数据域data是否等于要查找的值 x。如果相等,则表示找到了目标节点,直接返回 cur,即该节点的指针
  • cur = cur->next;如果当前节点的数据域不等于 x,则将cur指针更新为指向下一个节点,继续向后遍历链表。
  • return NULL;如果while循环结束后仍未找到值为x的节点,说明链表中不存在该值,此时返回 NULL

在test函数中实现时:

在这里插入图片描述

最后的打印结果为:
(将3修改为了30)
在这里插入图片描述

8.单链表在pos位置之插入x

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

问题:分析思考为什么不在pos位置之前插入?

原因:单链表的单向性决定了插入操作必须依赖前驱节点的遍历,若pos无法通过遍历到达(如越界或无前驱),则无法实现插入。

实例:若链表为1→2→3,想在节点2前插入0,需先找到节点1(前驱),再修改其指针为1→0→2→3。若无法找到前驱(如pos为4),则插入失败。

关键指针操作分析

  • newnode->next = pos->next;
    pos->next pos节点原本指向的下一个节点的指针。
    这行代码的作用是让新节点newnodenext指针指向pos节点原来的下一个节点。其目的是在插入新节点时,保证新节点能够连接到原链表中pos节点之后的部分,避免丢失后续的节点。可以把它理解为 “记住”pos节点后面的节点,以便后续正确连接链表。
  • pos->next = newnode;
    这行代码将pos节点的next指针指向新节点 newnode
    结合上一步,这就完成了将新节点插入到 pos 节点之后的操作。此时,pos 节点后面跟着新节点 newnode,而新节点 newnode 后面接着原链表中 pos 节点原本的后续节点,链表结构更新成功。

画图分析:
在这里插入图片描述

9.单链表删除pos位置之的值

// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	if (pos->next)//不为空
	{
		SListNode* next = pos->next;
		SListNode* nextnext =next->next;
		pos->next = nextnext;
		free(next);
	}
}

分析思考为什么不删除pos位置?

原因:在单链表中,直接删除pos位置节点需要遍历链表来寻找前驱节点,这会增加代码的复杂度和时间成本。因此,通常建议优先采用删除后继节点或者复制后继节点值的替代方案。如果确实需要删除pos节点,就必须传递头指针的地址(即二级指针),以便在头节点被删除时能够正确更新链表的头指针。

画图分析:
在这里插入图片描述
详细分析:
🖋️🖋️🖋️
pos->next也就是方框1的后半个位置,存放的是next的地址;通过next指向的next也就是第二个next的后半个方框,存放的是第三个nextnext的地址;所以删除后直接将nextnext所存放的地址赋值给pos->next!

如何去调用呢?

在这里插入图片描述

10.接口实现

在这里插入图片描述
🎉🎉🎉
到这里本章就结束啦~
我们下节见~

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

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

相关文章

深陷帕金森困境,怎样重燃生活信心?

帕金森,这个悄然影响无数中老年人生活的神经系统疾病,正逐渐走进大众视野。患病后,患者常出现静止性震颤,安静时手部、下肢不自主抖动,如同在默默诉说着身体的异常。肢体变得僵硬,行动迟缓,起步…

C语言(23)

字符串函数 11.strstr函数 1.1函数介绍: 头文件:string.h char *strstr ( const char * str1,const char *str2); 作用:在一个字符串(str1)中寻找另外一个字符串(str2)是否出现过 如果找到…

Docker运行hello-world镜像失败或超时:Unable to find image ‘hello-world:latest‘ locally Trying to pull reposi

Docker运行hello-world镜像失败或超时,报错:Unable to find image ‘hello-world:latest’ locally Trying to pull repository docker.io/library/hello-world … /usr/bin/docker-current: missing signature key. See ‘/usr/bin/docker-current run …

Linux内核如何和设备树协同工作的?

1.编写设备树 cd arch/riscv/boot/dts/ 再cd到厂商,例如下述内容。 2.编译设备树(dts->dtb)通过dtc命令来转换 3.解析设备树 例如上述内容,都是对设备树的解析。 这里重点说一下内核对设备树的处理吧,因为这个内…

LLM中的transformer结构学习(二 完结 Multi-Head Attention、Encoder、Decoder)

文章目录 LLM中的transformer结构学习(二 完结 Multi-Head Attention、Encoder、Decoder)Self-Attention (自注意力机制)结构多头注意力 EncoderAdd & Norm 层Feed Forward 层 EncoderDecoder的第一个Multi-Head AttentionMas…

高效编程指南:PyCharm与DeepSeek的完美结合

DeepSeek接入Pycharm 前几天DeepSeek的充值窗口又悄悄的开放了,这也就意味着我们又可以丝滑的使用DeepSeek的API进行各种辅助性工作了。本文我们来聊聊如何在代码编辑器中使用DeepSeek自动生成代码。 注:本文适用于所有的JetBrains开发工具&#xff0c…

爱可以传递,幸福可以具象化

遇到什么:晚上上课学生吵吵吵,把学生手机全部收了,放讲台上。 感受到的情绪:很烦躁。 反思:收手机也不是长久之计,可是物理有什么翻转课堂呢? 明天的待办事项:早上高数选修课&#x…

力扣-数组-367 有效的完全平方数

思路和时间复杂度 思路:利用二分,确定区间是左闭右闭,然后根据大小进行二分时间复杂度: 代码 class Solution { public:bool isPerfectSquare(int num) {bool flag false;if(num 0 || num 1) return true;long long …

Java关键字与标识符

Java关键字是预定义的保留字,用于定义程序结构和语义,如if、for、class等,不能用作标识符。JDK 8有50个关键字,JDK 11引入var用于局部变量类型推断。标识符用于命名变量、类等,由字母、数字、_、$组成,不能…

【神经网络】python实现神经网络(二)——正向推理的模拟演练

一.神经网络假设 在开始讲解之前,首先我们假设有这样一套神经网络,一共有三层: 其中,关于神经网络的权重、偏置的符号定义如下(如果不知道什么是权重和偏置,可以参考我之前写过的一篇文章:【机器学习】机器学习是什么意思): 以下文章将沿用以上这个设…

DeepSeek模型本地化部署方案及Python实现

DeepSeek实在是太火了,虽然经过扩容和调整,但反应依旧不稳定,甚至小圆圈转半天最后却提示“服务器繁忙,请稍后再试。” 故此,本文通过讲解在本地部署 DeepSeek并配合python代码实现,让你零成本搭建自己的AI…

Houdini SOP层 Scatter节点

SOP 代表 Surface Operator(几何体操作节点),所有几何体的建模、变形、分布等操作都在此层级完成。 Scatter节点的作用就是 以不同的密度在模型表面撒点 Scatter 节点属于 SOP(几何体)层级: 进入 Geometr…

【网络安全工程】任务12:网络安全设备

目录 一、防火墙​ 1、作用​ 2、配置方式​ 3、存在的漏洞​ 二、入侵检测系统(IDS)和入侵防御系统(IPS)​ 1、作用​ 2、配置方式​ 3、存在的漏洞​ 三、防病毒网关​ ​1、作用​ 2、配置方式​ 3、存在的漏洞​ …

Linux 进程控制:创建、终止、等待与程序替换全解析

亲爱的读者朋友们😃,此文开启知识盛宴与思想碰撞🎉。 快来参与讨论💬,点赞👍、收藏⭐、分享📤,共创活力社区。 目录 1.进程创建 1-1 fork函数初识​ 1-2 fork函数返回值​ 1-3…

SwiftUI 让视图自适应高度的 6 种方法(四)

概览 在 SwiftUI 的世界里,我们无数次都梦想着视图可以自动根据布局上下文“因势而变”‌。大多数情况下,SwiftUI 会将每个视图尺寸处理的井井有条,不过在某些时候我们还是得亲力亲为。 如上图所示,无论顶部 TabView 容器里子视图…

1035.不相交的线

1035.不相交的线 力扣题目链接(opens new window) 在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足: nums1[i] nums2[j]且绘制的直线…

2001-2023年上市公司数字化转型年报词频统计(年报词频统计和MDA词频统计两种方式)(吴非、赵宸宇、甄红线300+关键词三种方法)

2001-2023年上市公司数字化转型年报词频统计(年报词频统计和MD&A词频统计两种方式)(吴非、赵宸宇、甄红线300关键词三种方法) 1、时间:2001-2023年 2、来源:上市公司年报 3、参考文献: …

IO多路复用实现并发服务器

一.select函数 select 的调用注意事项 在使用 select 函数时,需要注意以下几个关键点: 1. 参数的修改与拷贝 readfds 等参数是结果参数 : select 函数会直接修改传入的 fd_set(如 readfds、writefds 和 exceptfds&#xf…

C++20 模块:告别头文件,迎接现代化的模块系统

文章目录 引言一、C20模块简介1.1 传统头文件的局限性1.2 模块的出现 二、模块的基本概念2.1 模块声明2.2 模块接口单元2.3 模块实现单元 三、模块的优势3.1 编译时间大幅减少3.2 更好的依赖管理3.3 命名空间隔离 四、如何使用C20模块4.1 编译器支持4.2 示例项目4.3 编译和运行…

有必要使用 Oracle 向量数据库吗?

向量数据库最主要的特点是让传统的只能基于具体值/关键字的数据检索,进化到了可以直接基于语义的数据检索。这在AI时代至关重要! 回到标题问题:是否有必要使用 Oracle 向量数据库? 这实际还要取决于你的具体应用需求。 客观来讲…