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

news2024/10/7 4:14:19

一.链表

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单链表

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

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

相关文章

数字化医学影像管理系统PACS源码

PACS系统&#xff0c;意为影像归档和通信系统。它是应用在医院影像科室的系统&#xff0c;主要的任务就是把日常产生的各种医学影像&#xff08;包括核磁&#xff0c;CT&#xff0c;超声&#xff0c;各种X光机&#xff0c;各种红外仪、显微仪等设备产生的图像&#xff09;通过各…

数据库选型与优化:策略与技巧的探讨

大家好&#xff0c;我是一名狂热的数据库程序员&#xff0c;最近鼓起勇气开始吐槽一下数据库&#xff0c;如有雷同&#xff0c;请对号入座。 名不副实的数据库类型 先说说最近的事&#xff0c;我们业务有很多图片要管理&#xff0c;老板说让我选个专业的图数据库&#xff0c;…

Leetcode—3.无重复字符的最长子串【中等】

2023每日刷题&#xff08;三十二&#xff09; Leetcode—3.无重复字符的最长子串 实现代码 class Solution { public:int lengthOfLongestSubstring(string s) {unordered_set<char> smap;int maxlen 0;int left 0;for(int i 0; i < s.size(); i) {while(smap.fi…

QT绘图设备

pixmap绘图设备在磁盘上进行绘图 通过pix.save将图片保存到E盘下 不是主要的绘画设备&#xff0c;可以将绘图指令保存 然后在下边可以调用重现绘图指令

Pikachu漏洞练习平台之SSRF(服务器端请求伪造)

注意区分CSRF和SSRF&#xff1a; CSRF&#xff1a;跨站请求伪造攻击&#xff0c;由客户端发起&#xff1b; SSRF&#xff1a;是服务器端请求伪造&#xff0c;由服务器发起。 SSRF形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能&#xff0c;但又没有对目标…

跨境电商测评新方案,安全可靠,高成功率

不管是做测评服务商还是卖家想给自己做测评&#xff0c;是否都为了如何搭建一个安全可靠的测评环境而苦恼呢&#xff1f;不知道如何搭建高成功率的真实安全买家环境苦恼&#xff1f;陈哥带你了解一种全新的解决方案&#xff0c;能够让你们的测评工作更加高效、安全、可靠&#…

简单介绍二分类问题评价指标

正确率(Accuracy) Accuracy ​(TP TN)/(TP TN FP FN)精准率(Precision) 记忆&#xff1a;在识别出某标签中正确的比例&#xff1b; 比如识别为某标签的一共有105个&#xff0c;其中有95个是识别对的&#xff0c;那Precision就是95/105&#xff1b; TP/(TPFP)召回率(Recall…

NJU操作系统公开课笔记(1)

目录 一.计算机系统概述 二.计算机硬件系统 三.计算机软件系统 四.计算机操作技术的发展 五.计算机OS 1.资源管理的角度 2. 程序控制的角度 3.OS控制计算机的角度 4.人机交互的角度 5.程序接口的角度 6.系统结构的角度 单道批处理系统 多道批处理系统 分时系统 …

Git 基本操作

目录 创建仓库命令 git init git clone 提交与修改 git add git status git diff git commit git reset git rm git mv git checkout git switch git restore 提交日志 git log git blame 远程操作 git remote git fetch git pull git push Git 的工作就…

Elasticsearch搜索分析引擎本地部署与远程访问

文章目录 系统环境1. Windows 安装Elasticsearch2. 本地访问Elasticsearch3. Windows 安装 Cpolar4. 创建Elasticsearch公网访问地址5. 远程访问Elasticsearch6. 设置固定二级子域名 Elasticsearch是一个基于Lucene库的分布式搜索和分析引擎&#xff0c;它提供了一个分布式、多…

深度学习(五)softmax 回归之:分类算法介绍,如何加载 Fashion-MINIST 数据集

Softmax 回归 基本原理 回归和分类&#xff0c;是两种深度学习常用方法。回归是对连续的预测&#xff08;比如我预测根据过去开奖列表下次双色球号&#xff09;&#xff0c;分类是预测离散的类别&#xff08;手写语音识别&#xff0c;图片识别&#xff09;。 现在我们已经对回…

Kafka学习笔记(三)

目录 第5章 Kafka监控&#xff08;Kafka Eagle&#xff09;5.2 修改kafka启动命令5.2 上传压缩包5.3 解压到本地5.4 进入刚才解压的目录5.5 将kafka-eagle-web-1.3.7-bin.tar.gz解压至/opt/module5.6 修改名称5.7 给启动文件执行权限5.8 修改配置文件5.9 添加环境变量5.10 启动…

【Proteus仿真】【51单片机】公交车报站系统

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使用LCD12864显示模块、DS18B20温度传感器、DS1302时钟模块、按键、LED蜂鸣器、ULN2003、28BYJ48步进电机模块等。 主要功能&#xff1a; 系统运行后&…

Linux基础知识(1)——目录结构,绝对/相对路径,指令等(配图)

目录 1.目录结构 2.路径 3.认识指令 文章内容并不聚焦于Linux命令&#xff0c;而是针对Linux的基础知识进行讲解&#xff0c;相信这部分知识更能帮助大家了解Linux系统。 本文只是带大家简单了解一下Linux的入门知识&#xff0c;在之后的文章中&#xff0c;我们将讲解Linux中…

【图数据库实战】HugeGraph架构

一、概述 作为一款通用的图数据库产品&#xff0c;HugeGraph需具备图数据的基本功能&#xff0c;如下图所示。HugeGraph包括三个层次的功能&#xff0c;分别是存储层、计算层和用户接口层。 HugeGraph支持OLTP和OLAP两种图计算类型&#xff0c;其中OLTP实现了Apache TinkerPop3…

C语言指针详解(1)(能看懂字就能明白系列)文章超长,慢慢品尝

目录 1、内存和地址 2、指针简介 与指针相关的运算符&#xff1a; 取地址操作符&#xff08;&&#xff09; 解引用操作符&#xff08;间接操作符&#xff09;&#xff08;*&#xff09; ​编辑 指针变量的声明 指针变量类型的意义 指针的基本操作 1、指针与整数相加…

解决 uniapp 开发微信小程序 不能使用本地图片作为背景图 问题

参考博文&#xff1a;uniapp微信小程序无法使用本地静态资源图片(背景图在真机不显示)的解决方法_javascript技巧_脚本之家 问题&#xff1a;uniapp 开发微信小程序&#xff0c;当使用本地图片作为 background-image 时&#xff0c;真机无法显示 解决&#xff1a; 方法一&am…

在线预览excel,luckysheet在vue项目中的使用

一. 需求 需要在内网项目中在线预览excel文档&#xff0c;并可以下载 二.在项目中下载并引入luckysheet 1.打开项目根目录&#xff0c;npm i luckyexcel 安装 npm i luckyexcel2.在项目的index.html文件中引入依赖 外网项目中的引入&#xff08;CDN引入&#xff09;&#…

Cesium:绘制地质剖面

作者:CSDN @ _乐多_ 本文记录了根据地质剖面的三角网的点、索引和颜色数组,绘制地质剖面的框架和部分代码。 效果如下图所示, 文章目录 一、算法逻辑二、代码一、算法逻辑 将三角网的点、索引和颜色数组按规则排列好,输入到第二节的代码中,可以绘制一个面。将这个绘制函…

如何确保消息不会丢失

本篇文章大家还可以通过浏览我的博客阅读。如何确保消息不会丢失 - 胤凯 (oyto.github.io)很多人刚开始接触消息队列的时候&#xff0c;最经常遇到的一个问题就是丢消息了。<!--more-->对于大部分业务来说&#xff0c;丢消息意味着丢数据&#xff0c;是完全无法接受的。 …