链表(一)----关于单链表的一切细节这里都有

一.链表

1 链表的概念及结构

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

  • 现实中的链表结构
    链表

  • 数据结构中的链表结构
    在这里插入图片描述

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


链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
在这里插入图片描述

二.实现单向链表

记住这个图,一会链表的逻辑会用到
在这里插入图片描述

我们创建三个文件:
头文件LList.h用于调用库函数、声明结构体和函数。
源文件LList.c存储函数。
源文件text.c进行测试。
每个源文件都必须包含LList.h。


1.声明链表结构体

//以下声明在头文件LList.h当中
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType val;
	struct SListNode* next;
}SLNode;

2.打印链表

声明

void SLTPrint(SLNode* phead);

SList.c

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

3.创建新节点

当我们进行插入节点等一系列操作时,都需要创建新的节点,用到这个函数

声明

SLNode* SLCreateNode(SLNDataType x);

SList.c

 SLNode* CreateNode(SLNDataType x)
 {
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode = NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->val = x;
	newnode->next = NULL;
	return newnode;
 }
  • 为新节点开辟空间,返回值为新节点的地址,所以函数类型为 SLNode* 结构体指针类型。
  • malloc函数为newnode开辟结构体大小个字节。
  • 判断是否开辟成功,失败则打印错误信息,结束函数运行。
  • 将新节点的数据val赋值为传入参数 x。
  • next赋值为空。

4.尾插

在这里插入图片描述
如果参数为一级指针,尾插传值只是临时拷贝,必须传地址

  • 声明
void SLTPushBack(SLNode** phead, SLNDataType x);

对结构体指针修改,要传地址,用二级指针接收
我们第一反应可能想当然敲出这样的经典错误:
在这里插入图片描述

  • 尾插的本质是上一个节点存下一个节点的地址
  • 而这里的tail是局部变量,出了作用域还会销毁,存在newnode内存泄漏的问题
  • tail和新节点确实在此刻取得了联系,但是并没有和上一个节点链接起来哦

  • SList.c
void SLTPushBack (SLNode ** pphead ,SLNDataType x)
{
	SLNode* newnode = CreateNode(x);
   //没有节点的情况
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else//有节点的情况
	{
		//找尾
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}
  • assert判断传入头节点指针的地址是否合法,为空则报错。
  • 为新节点newnode开辟空间并将x储存其中。
  • 插入时分两种情况:空链表 非空链表
  1. 如果链表为空则直接将*pphead 指向新节点 newnode,使其成为新的链表的头节点。
  2. 如果链表不为空,则创建变量tail指向头结点,循环遍历链表使tail指向尾节点,将新节点地址赋值给tail的next,成功将新节点添加到链表尾部。
    在这里插入图片描述
    plist、pphead和newnode的新空间关系如下
  • plist和phead都是在函数栈帧里面开辟
  • newnode是借助于malloc在堆上开辟的,是一个结构体指针,在开辟时数值域上放的其实就是尾插函数的第二个参数
    在这里插入图片描述

5.头插

  • 声明
void SLTPushFront(SLNode** pphead, SLNDataType x);
  • SList.c
void SLTPushFront(SLNode** pphead, SLNDataType x)
{
	SLNode* newnode = CreateNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
  • 注意:头插的这段代码对于没有节点的情况,既(*pphead)为空的情况也适用,所以不需要分类讨论
  • 创造出新节点的指针后,赋给newnode,此时newnode的数值域存放的也就是头插函数的第二个参数x,指针域存放的是NULL
  • 紧接着将*pphead,也就是plist,也就是原来第一个第一个节点的地址赋给newnode,这样,newnode就和原链表的节点取得了联系
  • 最后将newnode地址赋给*pphead,链表就顺利头插了!
    单链表的头插是非常方便的,这也是一个单链表的优点

6.尾删

  • 声明
void SLTPopBack(SLNode** pphead);
  • 当节点数量大于1的时候,用一级指针也可以,而头删的参数选择二级指针是因为当节点数只剩一个的时候,既是头也是尾,删除后要将*pphead置空,这样考虑的话就要对指针进行修改,所以索性传成二级指针了。

分析一波:

  • 尾删注意:

  • 在删除前必须保存一下即将删除节点的地址,这样的话才能free掉对应的内存空间,避免内存泄露的问题出现,所以我们还需要定义一个结构体指针prev,用来记录即将删除节点的地址

  • 当只有一个节点直接释放掉即可

  • 逻辑图如下:

在这里插入图片描述

  • 只有一个节点时,对应的代码这样写正确吗?
    在这里插入图片描述

这段代码的问题在于当只有一个节点的时候,prev和tail指向的是同一块空间,free掉tail之后,prev就变成了野指针
一定要理解free掉的是指针指向的内存空间,并不是把指针销毁了
而perv->next相当于对野指针访问了,所以是存在问题的

还存在一个问题,当节点都被删除完后,只剩一个NULL,如果继续删除,此时*pphead就为空,所以在删除前要对指针进行检查(断言 或者 if判断后提前return)。

  • SList.c
void SLTPopBack(SLNode** pphead)
{
	assert(*pphead);
	//1.一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else//一个以上的节点
	{
		//找尾
		SLNode* prev = NULL;
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;//可有可无,因为出了tail作用域,tail也会自动销毁
		prev->next = NULL;//必须置空,否则内存泄漏
	}
}

或者也可以这样写(只有else后的部分修改了)

void SLTPopBack(SLNode** pphead)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}

		free(tail->next);
		tail->next = NULL;
	}
}
  • 逻辑图如下:
    在这里插入图片描述

  • assert判断链表头节点是否为空,为空则报错。

  • 链表只有一个节点时,直接释放头节点空间,然后置空。

  • 链表有多个节点时,通过循环使变量 tail->next 找到尾节点,然后释放tail后一个节点的空间,也就是尾节点的空间,同时将其置空。


7.头删

  • 声明
void SLTPopFront(SLNode** pphead);

直接free掉头节点可以吗?

不行,当存在多个节点时,如果直接free第一个,后续的所有链表都访问不到了,内存也就随之泄漏了

先看看这个代码错哪里了?

在这里插入图片描述

在这里插入图片描述

tmp和*pphead指向的是同一块空间,free(tmp)后,*pphead成为了野指针
不要误认为free 掉 tmp后对 * pphead没有影响
但是上述代码的后两行换一下位置就对了

  • SList.c
void SLTPopFront(SLNode** pphead)
{
	assert(*pphead);
	
	SLNode* tmp = *pphead;
	
	*pphead = (*pphead)->next;
	
	free(tmp);
}

8.查找元素

  • 声明
SLNode* SLTFind( SLNode*phead,SLNDataType x);

查找不需要修改指针,传一级指针就可以了,遍历链表即可

  • SList.c
SLNode* SLTFind(SLNode* phead, SLNDataType x)
{
	SLNode* cur = phead;
	while (cur != NULL)
	{
		if (cur->val == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}
  • 函数在单链表中查找包含特定数据值 x 的节点。
  • 变量cur通过循环找到数据 val 等于x的节点。
  • 找到则返回指向当前节点的指针 cur,否则返回值为空。

9.在pos位置前插入一个元素

这个操作是单链表的一个劣势,因为单链表不支持随机访问,找下一个节点方便,但上一个节点并不好找

  • 声明
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x)

如果pos为第一个节点,相当于头插
直接调用头插函数 SLTPushFront(SLNode** pphead, SLNDataType x);

分析:

  • 需要找到pos的前一个节点地址,也就是pos前一个节点的指针,保存赋给prev,
  • 再在堆区上创建新的节点,将prev的指针域赋成新节点的地址,再将新节点的指针域赋成pos的地址,pos地址不需要提前保存,因为pos地址是参数提供好的。
    逻辑图如下:
    在这里插入图片描述
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
	// 严格限定pos一定是链表里面的一个有效节点
	assert(pphead);
	assert(pos);
	assert(*pphead);

	if (*pphead == pos)
	{
		// 头插
		SLTPushFront(pphead, x);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		SLNode* newnode = CreateNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}
  • 第一个assert判断传入头节点指针的地址是否合法,为空则报错。

  • 第二个assert判断传入指向链表中某个节点的指针pos是否合法,不存在则报错。

  • 如果在头节点位置之前插入,则调用头插解决。

  • 如果不是头节点位置,则创建一个指向链表头节点的指针 prev,然后使用循环找到要插入位置 pos 前面的节点。

  • 创建一个新的节点 newnode 并将数据值 x 存储在其中。

  • 修改 prev 节点的 next 指针,使其指向新节点 newnode,从而将新节点插入到 pos 前面


10.指定位置之后插入

  • 声明
void SLTInsertAfter(SLNode** pphead, SLNode* pos, SLNDataType x);

逻辑图如下:

在这里插入图片描述

  • SList.c
void SLInsertAfter(SLNode* pos, SLNDataType x)
{
	assert(pos);
	SLNode* newnode = BuyLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
  • 先创建好新的节点newnode
  • 将newnode的指针域赋成下一节点的地址,也就是原来pos的指针域
  • 将pos指针域赋值为newnode地址,完美插入。

11.删除pos位置的值

  • 声明
void SLTErase(SLNode** pphead, SLNode* pos);

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

  • SList.c
void SLTErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	if (*pphead == pos)
	{
		// 头删
		SLTPopFront(pphead);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

分析:

  • 第一个assert判断传入头节点指针的地址是否合法,为空则报错。
  • 第二个assert判断链表头节点是否为空,为空则报错。
  • pos节点为头节点,则调用头删解决。
  • pos不为头节点,则创建变量prev指向头节点,通过循环找到pos节点的前一个节点。
  • 将prev的next指向要删除的pos节点的下一个节点。
  • 释放pos空间

12.删除pos位置之后的值

  • 声明
void SLTEraseAfter(SLNode* pos);

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

  • Slist.c
void SLTEraseAfter(SLNode* pos)
{
	assert(pos);
	assert(pos->next);

	SLNode* tmp = pos->next;
	pos->next = pos->next->next;
-
	free(tmp);
	tmp = NULL;
}

13.销毁链表

  • 声明
void SLTDestroy(SLNode** pphead);

逻辑图如下:

在这里插入图片描述

  • SList.c
void SLTDestroy(SLNode** pphead)
{
	assert(pphead);

	SLNode* cur = *pphead;
	while (cur)
	{
		SLNode* next = cur->next;
		free(cur);
		cur = next;
	}

	*pphead = NULL;
}
  • 定义了两个指针,cur 和 tmp,用于遍历链表并释放内存。开始时,cur 被初始化为链表的头节点指针 pphead。

  • 这是一个循环,它会一直执行,直到 cur 变为 NULL,遍历到链表的末尾。

  • 在循环中,首先将 cur 赋值给 tmp,以便稍后释放 cur 指向的节点的内存。
    然后,将 cur 移动到下一个节点,即 cur = cur->next;

  • 最后,使用 free 函数释放 tmp 指向的节点的内存,即释放链表中的一个节点,接着进行循环依次释放节点直到链表最后。

所有完整版已经上传至我的gitte账户,

链接在这:gitee单链表

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

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

相关文章

开启数据库审计 db,extended级别或os级别)并将审计文件存放到/opt/oracle/audit/下

文章目录 1、登录到数据库2、查看审计状态3、创建审计目录4、启用审计5、设置审计文件路径5、再次查看结果 1、登录到数据库 使用SQL*Plus或者其他Oracle数据库客户端登录到数据库。 sqlplus / as sysdba;2、查看审计状态 show parameter audit;目前是DB状态&#xff0c;并且…

matplotlib 绘制双纵坐标轴图像

效果图&#xff1a; 代码&#xff1a; 由于使用了两组y axis&#xff0c;如果直接使用ax.legend绘制图例&#xff0c;会得到两个图例。而下面的代码将两个图例合并显示。 import matplotlib.pyplot as plt import numpy as npdata np.random.randint(low0,high5,size(3,4)) …

2023年最新十大地推拉新接单平台,都是一手单 官签渠道

2023年做拉新推广的地推人员&#xff0c;一定不要错过这十个接单平台&#xff0c;助你轻松找到一手单&#xff0c;这10个平台分别是 1. 聚量推客&#xff1a; “聚量推客”汇聚了众多市场上有的和没有的地推网推拉新接单项目&#xff0c;目前比较火热&#xff0c;我们做地推和…

【leaflet】学习笔记5 自定义控制层、多图层及其控制 重构

▒ 目录 ▒ &#x1f6eb; 导读开发环境 1️⃣ 重构data.js 数据抽取MyMap 面向对象编程继承MyMap类 2️⃣ d5. 自定义控制层、多图层及其控制示例效果自定义控制层多图层及其控制 &#x1f6ec; 文章小结&#x1f4d6; 参考资料 &#x1f6eb; 导读 开发环境 版本号描述文章…

电子病历编辑器源码(Springboot+原生HTML)

一、系统简介 本系统主要面向医院医生、护士&#xff0c;提供对住院病人的电子病历书写、保存、修改、打印等功能。本系统基于云端SaaS服务方式&#xff0c;通过浏览器方式访问和使用系统功能&#xff0c;提供电子病历在线制作、管理和使用的一体化电子病历解决方案&#xff0c…

CTFhub-RCE-过滤cat

查看当前目录&#xff1a;输入:127.0.0.1|ls 127.0.0.1|cat flag_42211411527984.php 无输出内容 使用单引号绕过 127.0.0.1|cat flag_42211411527984.php|base 64 使用双引号绕过 127.0.0.1|c""at flag_42211411527984.php|base64 使用特殊变量绕过 127.0.0.…

计算机毕业设计基于java+springboot+vue的实验室管理系统

项目介绍 系统中的功能模块主要是实现管理员&#xff1b;首页、个人中心、实验室管理、用户管理、实验室申请管理、设备管理、设备报备管理、设备申请管理、消耗品管理、消耗品领取管理、论坛管理、系统管理&#xff0c;用户前台&#xff1b;首页、实验室、设备、消耗品、论坛…

无需公众号实现微信JSSDK分享卡片!Safari浏览器分享到微信自动成卡片!

摘要 要在微信分享卡片&#xff0c;需要接入微信自家的JSSDK&#xff0c;比较麻烦&#xff0c;还需要认证公众号&#xff0c;但是如果你没有这样的条件&#xff0c;那么你也可以试试使用iOS的Safari浏览器轻松实现&#xff0c;只需要在html中加入3个meta即可。 代码 <!DO…

Linux(2):初探

Linux 是什么 Linux 就是一套操作系统。Linux 就是核心与系统呼叫接口那两层。 应用程序不算 Linux。 Linux 提供了一个完整的操作系统当中最底层的硬件控制与资源管理的完整架构&#xff0c; 这个架构是沿袭Unix 良好的传统来的&#xff0c;相当的稳定而功能强大。 在 Lin…

jQuery UI简单的讲解

我们先进入一下问答时间&#xff0c;你都知道多少呢&#xff1f; &#xff08;1&#xff09;什么是jQuery UI 呢&#xff1f; 解答&#xff1a;jQuery UI 是以 jQuery 为基础的开源 JavaScript 网页用户界面代码库。包含底层用户交互、动画、特效和可更换主题的可视控件。我们…

混合云运维解决方案,支持公有云、私有云、信创云等环境

数字时代&#xff0c;政企业务上云已成为大势所趋。虽然上云可为政企用户带来业务应用部署调度更加灵活、资源利用率更高的优点&#xff0c;但因云平台建设处于不同的阶段&#xff0c;且运转过程中包含大量的、不同类型的业务系统和应用场景&#xff0c;在整体云平台的建设中往…

EtherCAT 伺服控制功能块实现

EtherCAT 是运动控制领域主要的通信协议&#xff0c;开源EtherCAT 主站协议栈 IgH 和SOEM 两个项目&#xff0c;IgH 相对更普及一些&#xff0c;但是它是基于Linux 内核的方式&#xff0c;比SOEM更复杂一些。使用IgH 协议栈编写一个应用程序&#xff0c;控制EtherCAT 伺服电机驱…

ZYNQ_project:uart(odd,even)

概念&#xff1a; UART&#xff08;Universal Asynchronous Receiver-Transmitter&#xff09;&#xff1a;即通用异步收发器&#xff0c;是一种通用串行数据总线&#xff0c;用于异步通信。一般UART接口常指串口。 UART在发送数据时将并行数据转换成串行数据来传输&#xff…

注册表单mvc 含源代码

总结 jsp给我们的ControllerServlet.java,ControllerServlet.java获取参数,信息封装给RegisterFormBean.java的对象看是否符合格式,符合格式再信息封装给UserBean对象,调用Dbutil插入方法查重.]]要创建一个user集合成功跳哪个界面,打印信息注意什么时候要加getsession失败跳哪…

react-router-dom 版本6.18.0中NavLink的api和属性介绍

React Router 是一个基于 React 的路由库&#xff0c;它可以帮助我们在 React 应用中实现页面的切换和路由的管理。而 NavLink 则是 React Router 中的一个组件&#xff0c;它可以帮助我们实现导航栏的样式设置和路由跳转。 在 React Router 版本6.18.0 中&#xff0c;NavLink…

【用unity实现100个游戏之15】开发一个类保卫萝卜的Unity2D塔防游戏3(附项目源码)

文章目录 先看本次实现的最终效果前言绘制炮塔UI炮塔转向敌人生成炮弹旋转我们的子弹对敌人造成伤害&#xff0c;回收子弹自动发射子弹添加攻击间隔显示伤害字体设计通用泛型单例创建更多炮塔升级增加伤害升级缩短攻击间隔添加货币杀死敌人获取金币源码完结 先看本次实现的最终…

epoll协程简述

协程的由来 【协程第二话】协程和IO多路复用更配哦~_哔哩哔哩_bilibili 协程类别:有栈(静态)协程, 无栈(动态协程) 协程epoll 当有需要等待的时候,就切换出去,要用汇编保存这个栈rsp 运行时,要根据协程上下文恢复出这个栈

Beego之Bee工具使用

1、bee工具使用 bee 工具是一个为了协助快速开发 Beego 项目而创建的项目&#xff0c;通过 bee 你可以很容易的进行 Beego 项目的创 建、热编译、开发、测试、和部署。Bee工具可以使用的命令&#xff1a; [rootzsx ~]# bee 2023/02/18 18:17:26.196 [D] init global config…

Java基础笔记

1.数据类型在java语言中包括两种: 第一种:基本数据类型 基本数据类型又可以划分为4大类8小种: 第一类:整数型 byte , short,int, long(没有小数的&#xff09; 第二类:浮点型 float,aouble(带有小数的&#xff09; 第三类:布尔型 boole…

【Rust】快速教程——模块mod与跨文件

前言 道尊&#xff1a;没有办法&#xff0c;你的法力已经消失&#xff0c;我的法力所剩无几&#xff0c;除非咱们重新修行&#xff0c;在这个世界里取得更多法力之后&#xff0c;或许有办法下降。——《拔魔》 \;\\\;\\\; 目录 前言跨文件mod多文件mod 跨文件mod //my_mod.rs…