数据结构与算法:单链表

朋友们大家好,本节来到数据结构与算法的新内容:单链表

在上篇文章中,我们知道顺序表通常需要预分配一个固定大小的内存空间
通常以二倍的大小进行增容,可能会造成空间的浪费,本篇文章我们介绍的链表可以解决这个问题

单链表

  • 链表的定义和结构
    • 单链表的创建
    • 链表的打印
    • 创造节点
    • 单链表的尾插和头插
      • 尾插
      • 头插
    • 单链表的尾删和头删
      • 尾删
      • 头删
    • 寻找某个节点
    • 在指定位置后面插入节点
    • 在指定位置前面插入节点
    • 在指定位置后面删除节点
    • 在指定位置前面删除节点

链表的定义和结构

链表是一种在计算机科学中常用的数据结构,用于存储元素的集合。它与数组相比,链表的元素不是在内存中连续存储的。链表由一系列节点组成,每个节点至少包含两个部分:一个是存储的数据,另一个是指向列表中下一个节点的指针(或引用)。通过这种方式,链表中的节点被串联起来

在这里插入图片描述

在顺序表中,我们的数据存储在数组中,每个数据在内存中连续存储,意味着可以通过索引直接访问任何元素
在这里插入图片描述
我们对顺序表进行数据的插入,物理空间不变,数据依次挪动。

在这里插入图片描述

链表中,我们每个节点的地址没有关联,是随机的,但是每个节点都有一个**指针,**让这个指针指向下一个节点的地址

我们设phead指针指向第一个地址,第一个节点的指针指向第二个节点的地址,最后一个节点的指针指向空

如果我们想在2 3节点中间插入新的数据a,并不需要挪动任何数据,只需要将2的指针指向a的地址,a的指针指向3的地址
在这里插入图片描述
若要删除3的数据,我们只需将2的指针指向4,并将3的空间释放掉即可

单链表的创建

typedef int SLNDataType;

typedef struct SListNode
{
	SLNDataType val;
	struct SListNode* next;
}SLNode;

其中typedef int SLNDataType;在顺序表中我们进行了讲述,为类型抽象

typedef struct SListNode
{
	SLNDataType val;
	struct SListNode* next;
}SLNode;
  • val 用于存储节点中的数据。数据类型由 SLNDataType 定义,当前是 int 类型,表示每个节点可以存储一个整数。
  • next 是一个指向下一个 SListNode 结构体的指针。它用于链接链表中的节点,使得可以从一个节点遍历到下一个节点。如果 next 为 NULL,则表示当前节点是链表的末尾。

链表的打印

我们定义了一个函数,它的作用是打印出一个单链表的所有节点值

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

我们来解析这个函数:

  • 首先,函数内部定义了一个指针 cur 并将它初始化为指向链表的头节点 phead。这个指针用于遍历链表。

  • 接着,函数进入一个 while 循环,条件是 cur != NULL,即只要 cur 指向的节点不是 NULL,循环就继续执行。这确保了函数能够遍历链表直到最后一个节点。

  • 在循环体内,使用 printf 函数打印当前节点 cur 存储的整数值 cur->val,后面跟着一个箭头 ->,指示链表中的节点是如何连接的。

  • 然后,cur 被更新为指向下一个节点 cur->next,准备打印下一个节点的值。这个步骤使得遍历可以继续进行。

  • 一旦遍历完成(即 cur 为 NULL),循环结束。此时打印 “NULL” 来表示链表的结束

在后续我们会展示出来

创造节点

我们定义一个函数 CreatNode,用于创建一个新的链表节点 SLNode;

SLNode* CreatNode(SLNDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->val = x; 
	newnode->next = NULL;
	return newnode;
}

CreatNode 函数的目的是为了构造一个新的链表节点,并将其初始化。它接收一个参数 x,类型为 SLNDataType,这是链表节点存储数据的类型。函数的返回类型是 SLNode*,即新创建的链表节点的指针。我们来解析这个函数

  • 通过调用 malloc(sizeof(SLNode)) 分配足够的内存来存储一个 SLNode 结构体实- 例。malloc 函数尝试分配指定大小的内存空间,并返回指向这块内存的指针。如果分配成功,这个指针指向的是新分配的内存;如果失败,则返回 NULL。

  • **初始化节点:**为新节点的 val 成员赋值参数 x,这表示节点存储的数据。将 next 成员设置为 NULL,表示新节点当前没有指向下一个节点

  • 返回新节点:函数返回新创建的节点的指针,允许调用者将该节点添加到链表中的适当位置。

这个 CreatNode 函数用于创建并初始化链表的新节点。通过接收一个数据作为参数,并返回指向新分配和初始化的节点的指针,它为链表构造、数据插入等提供了基础。

单链表的尾插和头插

尾插

首先进行思考,我们能否这样定义函数?

void SLTPushBack(SLNode* pphead, SLNDataType x);

如果链表不为空,我们可以如下找到尾部

SLNode* newnode = CreatNode(x);
SLNode* tail = pphead;
while (tail->next != NULL) {
	tail = tail->next;
}
tail->next = newnode;

这里并没有对头指针进行任何的修改

如果链表为空,意味着我们需要将头指针指向空改为指向newnode,

我们想对一个数值通过函数修改,需要指针,那么对一个指针进行修改,则需要二级指针

所以函数定义如下:

void SLTPushBack(SLNode** pphead, SLNDataType x) {
	SLNode* newnode = CreatNode(x);
	if (*pphead == NULL) {
		*pphead = newnode;
	}
	else 
	{
		SLNode* tail = *pphead;
		while (tail->next != NULL) {
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

这里,如果链表为空,插入新节点,则头指针发生了改变

我们再test文件中进行测试,创建并初始化头指针plist
在这里插入图片描述
尾插三次,插入1 2 3并打印,结果如下

在这里插入图片描述

头插

头插我们会改变头指针,也需要二级指针

void SLTPushFront(SLNode** pphead, SLNDataType x)
{

	SLNode* newnode = CreatNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

在这里插入图片描述
我们用pphead接收plist

  • newnode的指针next指向d1,即plist(*pphead)newnode->next = *pphead;,再让plist(*pphead)指向newnode。*pphead = newnode;

在这里,链表为空也无所谓,newnode指向NULL,plist指向newnode

测试如下
在这里插入图片描述

单链表的尾删和头删

尾删

void SLTPopBack(SLNode** pphead) 
{
	if (*pphead == NULL) {
		return; 
	}
	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;
		return;
	}
	SLNode* prev = *pphead;
	while (prev->next->next != NULL) { 
		prev = prev->next;
	}
	free(prev->next); 
	prev->next = NULL;
}
  • 如果头指针指向 NULL,即链表没有节点,函数将直接返回,因为没有节点可以删除。

  • if ((*pphead)->next == NULL) {:这一行检查链表是否只有一个节点。它通过检查头节点的 next 指针是否为 NULL 来实现。如果是这种情况,说明链表中只有一个节点。free(*pphead);:如果链表只有一个节点,这行代码释放这个节点占用的内存。
    pphead = NULL;:由于最后一个节点已被删除,链表现在为空。因此,需要将头指针设置为 NULL。

  • 如果有多个节点,这个 while 循环遍历链表,直到 prev 指向倒数第二个节点。条件 prev->next->next != NULL 确保了 prev 停在倒数第二个节点,因为倒数第二个节点的下一个节点(即最后一个节点)的 next 指针是 NULL。 free(prev->next);:释放最后一个节点占用的内存。此时,prev->next 指向最后一个节点。
    prev->next = NULL;:将倒数第二个节点的 next 指针设置为 NULL,从而移除对最后一个节点的引用,更新链表的末尾

测试如下,每次尾删后进行打印
在这里插入图片描述

头删

void SLTPopFront(SLNode** pphead) {
	if (*pphead == NULL) {
		return; 
	}
	SLNode* temp = *pphead;
	*pphead = (*pphead)->next;
	free(temp);
}

如果链表为空,直接返回,如果不为空,temp和*pphead现在指向同一块空间,我们让头指针指向第二个节点,现在只有temp指向第一个节点,释放第一个节点的空间

测试代码
在这里插入图片描述

寻找某个节点

SLNode* SLTFind(SLNode* phead, SLNDataType x) 
{
	SLNode* current = phead; 
	while (current != NULL) {
		if (current->val == x) {
			return current; 
		}
		current = current->next; 
	}
	return NULL; 
}

初始化一个名为 current 的指针,用于遍历链表

在循环体内,if (current->val == x) 检查当前节点的值是否为目标值 x,若当前节点的值不是目标值,则通过 current = current->next; 将 current 更新为下一个节点的指针,继续遍历链表。

在指定位置后面插入节点

void SLTInsertAfter(SLNode* pos, SLNDataType x) {
	if (pos == NULL) {
		return;
	}
	SLNode* newnode =CreatNode(x);
	newnode->next = pos->next; 
	pos->next = newnode; 
}
  • 首先检查pos是否为NULL,如果是,则不进行插入
  • 若不为空,则让newnode指向pos原来指向的节点,pos指向newnode完成插入

测试如下,我们已经有了三个数据1 2 3,在2后面插入4
在这里插入图片描述
首先找到2节点的地址,再传入insert函数中即可

在指定位置前面插入节点

要在指定位置之前插入一个新节点,情况就稍微复杂一点,因为单向链表的节点只包含指向下一个节点的指针,没有指向前一个节点的指针。这意味着,要实现这一功能,你需要找到目标位置的前一个节点,然后在这个节点之后插入新节点。以下是实现这一功能的方法

void SLTInsertBefore(SLNode** phead, SLNode* pos, SLNDataType x) {

	if (*phead == NULL || pos == NULL || *phead == pos) {
		SLNode* newnode = CreatNode(x);
		if (*phead == pos) {
			newnode->next = *phead;
			*phead = newnode;
		}
		else {
			free(newnode); 
		}
		return;
	}

	SLNode* current = *phead;
	while (current->next != NULL && current->next != pos) {
		current = current->next;
	}
	if (current->next != pos) {
		return;
	}
	SLNode* newnode = CreatNode(x);
	newnode->next = pos;
	current->next = newnode;
}
  1. 首先检查链表是否为空,或者目标位置是否是链表的第一个节点
    • 如果是第一个节点,则意味着头插

    • 如果pos为NULL,表示在空链表中插入,或者pos不在链表中
      将创建的newnode释放掉

if (*phead == NULL || pos == NULL || *phead == pos) {
		SLNode* newnode = CreatNode(x);
		if (*phead == pos) {
			newnode->next = *phead;
			*phead = newnode;
		}
		else {
			free(newnode); 
		}
		return;
	}

  1. 寻找pos前的节点
SLNode* current = *phead;
while (current->next != NULL && current->next != pos) {
	current = current->next;
}

如果没有找到pos,则不执行插入

if (current->next != pos) {
        return;
    }
  1. 在pos前插入新节点
SLNode* newnode = CreatNode(x);
	newnode->next = pos;
	current->next = newnode;
}

测试代码如下:在1 2 3的2前面插入4
在这里插入图片描述
首先找到2节点的地址再传入插入函数

在指定位置后面删除节点

void SLTEraseAfter(SLNode* pos) {
    if (pos == NULL || pos->next == NULL) {
        return;
    }
    SLNode* temp = pos->next;
    pos->next = temp->next;
    free(temp);
}
  1. 首先检查pos是否为空或pos之后没有节点,因为如果 pos 是 NULL,则没有任何节点可操作;如果 pos->next 是 NULL,则 pos 是链表中的最后一个节点,没有节点需要被删除。

  2. 将**待删除的节点(pos->next)**的地址存储在一个临时变量中。

  3. 更新 pos->next 指针,使其指向待删除节点的下一个节点。

  4. 释放待删除节点的内存,以防止内存泄漏。

测试代码如下,删除1 2 3中2后面的数据
在这里插入图片描述

在指定位置前面删除节点

在单向链表中删除某个指定位置pos之前的节点比删除其之后的节点稍微复杂,因为链表的单向性质意味着不能直接访问前驱节点。为了删除给定pos之前的一个节点,我们首先需要找到这个节点的前两个节点(即要删除的节点的前驱节点和要删除的节点)。然后,通过调整前驱节点的next指针,绕过要删除的节点,最后释放要删除的节点的内存。

void SLTEraseBefore(SLNode** phead, SLNode* pos) {

	if (*phead == NULL || pos == NULL || *phead == pos) return;

	if ((*phead)->next == pos) {
		SLNode* temp = *phead;
		*phead = (*phead)->next; 
		free(temp);
		return;
	}
	SLNode* prev = NULL;
	SLNode* current = *phead;
	while (current != NULL && current->next != pos) {
		prev = current;
		current = current->next;
	}
	if (current == NULL || current->next != pos) return;
	if (prev != NULL) {
		prev->next = current->next;
		free(current);
	}
}

如果链表为空,pos为空,或pos是链表的第一个节点,无法删除前一个节点

if (*phead == NULL || pos == NULL || *phead == pos) return;
if ((*phead)->next == pos) {
		SLNode* temp = *phead;
		*phead = (*phead)->next; // 移动头指针
		free(temp);
		return;
	}

这段代码的目的是处理一个特殊的情况:当我们想要删除位于给定pos节点之前的节点,且该pos恰好是链表的第二个节点时。下面分步解释这个代码块的含义和作用:

  1. 条件检查 (if ((*phead)->next == pos)): 这个条件检查链表的头节点(*phead)的下一个节点是否就是我们的目标位置pos。如果是,这意味着我们需要删除链表的第一个节点(即头节点),因为我们想要删除的是位于pos之前的节点,且pos是链表的第二个节点。

  2. 删除头节点:

    • SLNode* temp = *phead;:首先,我们将头节点(要删除的节点)的地址保存在临时变量temp中,以便之后能够安全地释放该节点占用的内存。
    • *phead = (*phead)->next;:接着,我们将头指针(*phead)向前移动到下一个节点,这实际上是在更新链表的头节点为原头节点的下一个节点。简单来说,我们就是把头指针指向了链表的第二个节点,从而在逻辑上“删除”了原来的第一个节点(头节点)。
  3. 释放内存 (free(temp);: 最后,使用free(temp);来释放原头节点占用的内存。

SLNode* prev = NULL;
	SLNode* current = *phead;
	while (current != NULL && current->next != pos) {
		prev = current;
		current = current->next;
	}

找到pos之前的节点

if (current == NULL || current->next != pos) return;

无法找到有效的pos位置,pos不在列表中,则返回

if (prev != NULL) {
	prev->next = current->next;
	free(current);
}

删除current的节点,即pos之前的节点;
测试代码如下,删除1 2 3中2前面的节点
在这里插入图片描述
本节内容到此结束,感谢大家观看!

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

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

相关文章

去除vue自带的边距

使用vue时发现总有去不掉的外边距&#xff0c;在index.vue里面怎样设置样式都不管用 查阅资料后发现要在vue项目自带的index.html文件内添加下面的样式代码才行 <style>*{margin: 0;padding: 0;}body,html{margin: 0;padding: 0;} </style>

二、DataX安装

DataX安装 一、简介二、系统要求三、部署 一、简介 官方地址&#xff1a;https://github.com/alibaba/DataX/blob/master/userGuid.md 二、系统要求 LinuxJDK(1.8以上&#xff0c;推荐1.8) Centos7.9的java1.8安装命令&#xff1a;yum install java-1.8.0-openjdk.x86_64 Py…

【新书推荐】7.6节 相对基址加变址寻址方式

本节内容&#xff1a;基址加变址寻址方式。 ■基址加变址寻址方式&#xff1a;指令操作数为内操作数&#xff0c;操作数地址使用[基址寄存器变址寄存器]表示。 7.6.1 基址加变址寻址方式 基址加变址寻址方式的操作数在存储器中&#xff0c;操作数的有效地址由基地址寄存器&am…

day 20(补2.5)

fread 函数&#xff1a; 今日练习 C语言面试题5道~ 1. static 有什么用途&#xff1f;&#xff08;请至少说明两种&#xff09; 1) 限制变量的作用域 2) 设置变量的存储域 2. 引用与指针有什么区别&#xff1f; 1) 引用必须被初始化&#xff0c;指针不必。 2) 引用初始…

幻兽帕鲁服务器原来的存档不想玩了,怎么清档?如何重来?

如果需要备份原存档的话&#xff0c;就先把存档导出来备份。或者手动去服务器文件里找到游戏存档文件夹&#xff0c;保存下载。 如无需备份原存档&#xff0c;则可以直接使用幻兽帕鲁应用模板&#xff0c;来重装服务器的操作系统。 方法很简单&#xff1a; 详细教程地…

mysql经典4张表问题

1.数据库表结构关联图 2.问题&#xff1a; 1、查询"01"课程比"02"课程成绩高的学生的信息及课程分数3.查询平均成绩大于等于60分的同学的学生编号和学生姓名和平均成绩4、查询名字中含有"风"字的学生信息5、查询课程名称为"数学"&…

网络协议与攻击模拟_17HTTPS 协议

HTTPShttpssl/tls 1、加密算法 2、PKI&#xff08;公钥基础设施&#xff09; 3、证书 4、部署HTTPS服务器 部署CA证书服务器 5、分析HTTPS流量 分析TLS的交互过程 一、HTTPS协议 在http的通道上增加了安全性&#xff0c;传输过程通过加密和身份认证来确保传输安全性 1、TLS …

【制作100个unity游戏之25】3D背包、库存、制作、快捷栏、存储系统、砍伐树木获取资源、随机战利品宝箱3(附带项目源码)

效果演示 文章目录 效果演示系列目录前言丢弃物品源码完结 系列目录 前言 欢迎来到【制作100个Unity游戏】系列&#xff01;本系列将引导您一步步学习如何使用Unity开发各种类型的游戏。在这第25篇中&#xff0c;我们将探索如何用unity制作一个3D背包、库存、制作、快捷栏、存…

netstat命令

netstat 是一个计算机网络命令行工具&#xff0c;用于显示网络连接、路由表和网络接口等网络相关信息。netstat 命令可以在各种操作系统上使用&#xff0c;包括 Windows、Linux 和 macOS 等。 在使用 netstat 命令时&#xff0c;可以提供不同的选项来显示不同类型的网络信息。…

助力智能化农田作物除草,基于轻量级YOLOv8n开发构建农田作物场景下玉米苗、杂草检测识别分析系统

在我们前面的系列博文中&#xff0c;关于田间作物场景下的作物、杂草检测已经有过相关的开发实践了&#xff0c;结合智能化的设备可以实现只能除草等操作&#xff0c;玉米作物场景下的杂草检测我们则少有涉及&#xff0c;这里本文的主要目的就是想要基于最新的YOLOv8下最轻量级…

微信问一问·流量赚钱专栏

微信问一问流量赚钱专栏 1493读者&#xff0c;104内容 看专栏解百惑&#xff0c;赚到钱 问一问免费涨粉利器 独家更新 100 篇「问一问」经验贴。带你入门&#xff0c;解惑&#xff0c;提效&#xff0c;涨粉&#xff0c;赚小钱 零成本单日公众号涨粉 1000 &#xff0c;专栏成…

《Java 简易速速上手小册》第9章:Java 开发工具和框架 (2024 最新版)

文章目录 9.1 Maven 和 Gradle - 构建与依赖管理的神兵利器9.1.1 基础知识9.1.2 重点案例&#xff1a;使用 Maven 构建 Spring Boot 应用9.1.3 拓展案例 1&#xff1a;使用 Gradle 构建多模块项目9.1.4 拓展案例 2&#xff1a;利用 Gradle Wrapper 确保构建的一致性 9.2 Spring…

【数据结构】哈希表的开散列和闭散列模拟

哈希思想 在顺序和树状结构中&#xff0c;元素的存储与其存储位置之间是没有对应关系&#xff0c;因此在查找一个元素时&#xff0c;必须要经过多次的比较。 顺序查找的时间复杂度为0(N)&#xff0c;树的查找时间复杂度为log(N)。 我们最希望的搜索方式&#xff1a;通过元素的…

计算x的平方根x含负数和复数cmath.sqrt(x)

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 计算x的平方根 x含负数和复数 cmath.sqrt(x) cmath.sqrt(-4)输出的结果是&#xff1f; import cmath import math a 4 print("【显示】a ",a) print("【执行】math.sqrt(a)&…

InstantBox:开箱即用的临时 Linux 环境

在云计算和虚拟化技术日益成熟的今天&#xff0c;我们有时需要一个快速、简单、临时的 Linux 环境来进行各种任务。这就是 InstantBox 的用武之地。 什么是 InstantBox&#xff1f; InstantBox 是一个开源项目&#xff0c;它可以快速启动临时的 Linux 系统&#xff0c;并提供…

HeidiSQL安装配置(基于小皮面板(phpstudy))连接MySQL

下载资源 对于这款图形化工具&#xff0c;博主建议通过小皮面板&#xff08;phpstudy&#xff09;来下载即可&#xff0c;也是防止你下载到钓鱼软件&#xff0c;小皮面板&#xff08;phpstudy&#xff09;如果你不懂是什么&#xff0c;请看下面链接这篇博客 第二篇&#xff1a;…

Vision Transformer Pytorch 实现代码学习记录

目前运营的社交平台账号&#xff1a; CSDN 【雪天鱼】: 雪天鱼-CSDN博客哔哩哔哩 【雪天鱼】: 雪天鱼个人主页-bilibili.com 可能后续有更新&#xff0c;也可能没有更新&#xff0c;谨慎参考 V1.0 24-02-13 ViT 代码的基本训练, 预测推理脚本运行 1 学习目标 能用官方的 ViT…

React18原理: 核心包结构与两大工作循环

React核心包结构 1 ) react react基础包&#xff0c;只提供定义 react组件(ReactElement)的必要函数一般来说需要和渲染器(react-dom,react-native)一同使用在编写react应用的代码时, 大部分都是调用此包的api比如, 我们定义组件的时候&#xff0c;就是它提供的class Demo ext…

Stream Query Denoising for Vectorized HD Map Construction

参考代码&#xff1a;截止2024.02未开源 动机与出发点 这篇文章是在StreamMapNet的基础上做的&#xff0c;为了在局部地图感知任务上提升时序上的感知稳定性&#xff0c;参考DN-DETR中的去噪方案&#xff0c;为局部地图感知提出一种针对局部地图元素的加噪声方案以及去噪逻辑。…

在线JSON解析格式化工具

在线JSON解析格式化工具 - BTool在线工具软件&#xff0c;为开发者提供方便。JSON在线可视化工具:提供JSON视图,JSON格式化视图,JSON可视化,JSON美化,JSON美化视图,JSON在线美化,JSON结构化,JSON格式化,JSON中文Unicode等等。以清晰美观的结构化视图来展示json,可伸缩折叠展示,…