不懂单链表? 这篇文章就帮你搞明白

news2024/12/23 0:43:35

坚持看完,结尾有思维导图总结

链表对指针的操作要求不低

  • 链表的概念
  • 链表的特性
  • 链表的功能(最重要)
    • 定义和初始化
    • 头插头删
      • 细节说明
    • 尾插尾删
    • 寻找链表元素与打印链表
    • 在 某位置后插入删除
    • 在某位置的插入删除
    • 销毁链表

链表的概念

什么是链表

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

解释一下
通俗的链表解释是 通过箭头链接的
在这里插入图片描述就如同锁链一样链接起来

两个节点之间的空间都没有任何联系
比如 节点A 和节点 B的空间 本来没有任何联系

但是两者通过 next 的指针

从节点 A 能够访问到节点 B ,然后能够一直向下访问知道 空指针

但是实际上
那个箭头是不存在的,是用来理解用的

实际存在的只有地址,访问过程中调用的也是指针(地址)
在这里插入图片描述
从图片的分析来看
一个节点要被分成两半
一般用来储存数据,一般用来储存下一个节点的地址
所以我们程序上的指针的定义可以这样写

typedef int Datatype;
typedef struct SingleList
{
	Datatype Data;
	struct SingleList* next;
}SLTNode;

但是下面定义是不对的

typedef int Datatype;
typedef struct SLTNode
{
	Datatype data;
	SLTNode* next;
}SLTNode;

因为在内容上 next 的定义在 SLTNode 的 typedef 重命名前,语法错误

链表的特性

链表有什么独特的地方?

所谓独特,必须有所对比,这里取顺序表进行对比

不同点链表顺序表
存储空间物理上空间连续逻辑连续,物理上不一定连续
随机访问可以使用下标直接访问必须遍历找到对应地址
任意位置插入(重要)需要搬移数据只需要修改指向
容量容量不足的时候需要扩容不需要扩容
使用场景元素频繁访问任意位置的插入和删除

所以链表的优点

  1. 插入删除很高效
  2. 寻找元素速度慢
  3. 空间利用率高,不需要扩容

链表的功能(最重要)

我们如何利用链表存储数据和调用数据?

链表本身就是一个数据结构,就需要定义定义一个数据结构,并且实例化
数据结构用来存放取出数据,访问数据
最终要销毁数据结构
所以有如下要求

定义和初始化

第一个问题,链表是由什么组成的?
是由 内容为存储的数据 和 指向下一个节点的 节点组成
第1. 1 的问题是 节点的类型 如何定义?
在这里插入图片描述
用程序的话说就是:

typedef int STDatatype;

struct ListNode
{
	STDatatype data;
	struct ListNode* next;
};
typedef struct ListNode SLTNode;

但是只有一个节点的结构是不足的
就像只知道有 int 这个类型但是没有数据一样,光有空壳但是没有血肉
如何在 Node 里面填充血肉?
第1.2 个问题:如何创建节点
主要步骤是:

  1. 开辟一个节点空间
  2. 在节点空间中存放数据

对于程序来说就是:

SLTNode* BuyNewNode(Datatype x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if(newnode == NULL)
	{
		perror("malloc failed");
		exit(-1);//故障退出
	}
	newnode->next = NULL;
	newnode->data = x;
	return newnode;
}

第1.3 个问题: 如何初始化一个链表

我们使用单链表的时候,只要拿到链表的头就能够操作了
所以因为头的特殊性,我们单独定义一个名字

typedef SLTNode SLTHead;

随后进行链表的创建
步骤

  1. 循环创建节点
  2. 将节点链接起来

细节

  1. 当创建第一个节点时,头结点改变指向第一个节点,之后的节点头结点位置不再改变
//创建 n 个节点
SLTHead* BuildList(int n )
{
	SLTHead* phead = NULL;
	SLTNode* pcur = phead;
	for(int i = 0;i<n;i++)
	{
		SLTNode* newnode = BuyNewNode(i);
		if(phead == NULL)
		{
			phead = newnode;
			pcur = phead;
		}
		else
		{
			pcur->next = newnode;
			pcur = newnode;
		}
	}
	return phead;
}

我们创建链表的工作就完成了,我们可以调用一下

void Test0()
{
	//创建一个有10个节点的链表,保留他的头节点
	SLTHead* pList = BuildList(10);
}

头插头删

链表有两端,头为一端,尾为一端
头尾的图
在这里插入图片描述
要实现头部的插入删除,我们要解决以下问题
第1.1个小方面,头插
第1.1.1个问题
头插的过程是什么样的?
步骤

  1. 生成一个新节点newnode
  2. 这个新节点链接到原来的链表
  3. 链表头向前移动
    在这里插入图片描述
    程序

第一种程序

void SLTPushFront(SLTHead* phead,Datatype x)
{
	SLTNode* newhead = 	BuyNewNode(x);
	newnode->next = phead;
	phead = newnode;	
}

这个程序是错误的,哪里错了呢?
(这个图片需要放大一点看)
在这里插入图片描述
调用函数,创建栈帧的时候
开辟空间把实参的值传给形参
最终函数调用完成后会释放栈帧,销毁变量
如果要改变主函数的类型,就需要传递对应的类型的指针
因为我们要改变的是 头节点的地址,就必须要传形参为指针的地址

正确的程序:

void SLTPushFront(SLTHead** pphead,Datatype x)
{
	assert(pphead);
	SLTNode* newnode = BuyNewNode(x);
	//没有必要对 pcur 判空
	SLTNode* pcur = *pphead;
	newnode->next = pcur;
	*pphead = newnode;
}

我们可以看到,即使链表本来为 null 我们也可以直接进行头插,不需要特殊处理

第1.2个方面,头删
第1.2.个问题
链表的头删是如何工作的?
步骤

  1. 找到头节点的下一个节点
  2. 释放头结点
  3. 令头结点为下一个节点

图解:
在这里插入图片描述
第1.2.2个问题
如果链表被删除到空,该做些什么?
如果是 链表已经为 空,那不能再删除,我们可以使用assert来截断
程序

void SLTPopFront(SLTHead** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLTNode* pcur = *pphead;
	SLTNode* plast = pcur;
	pcur = pcur->next;
	free(plast);
	*pphead = pcur;
}

难点:
对头指针的改变需要传递指针的地址

细节说明

assert的使用
为什么使用assert ?
因为,在assert报错的时候,程序会定位到报错的位置
在调试的时候出乎意料地节省很多很多时间
(真地非常好用)

尾插尾删

第1.1 个问题,尾插的步骤是什么样子的?

  1. 找到尾
  2. 向尾部插入数据
    图解:
    在这里插入图片描述

第1.2 个问题,尾插的难点是什么?
1 如果链表本身没有元素
如果没有元素,就需要改变主函数中的头结点指针,因为要改变头结点指针,所以要传指针的地址
2 找到尾巴的过程
依次遍历,直到 next 为空

程序:

void SLTPushBack(SLTHead** pphead,Datatype x)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	SLTNode* newnode = BuyNewNode(x);
	if(pcur == NULL)
	{
		*pphead = newnode;
		return;
	}
	while(pcur->next)
	{
		pcur = pcur->next;
	}
	pcur->next = newnode;
}

第2.1 个问题,尾删的步骤是什么样子的?
步骤
1 找到尾
2 然后把尾巴删除

第2.2个问题,尾删的难点是什么?
1 上一个节点如果的 next 如果置为空,就无法找到下一个节点,找到下一个节点就无法回溯到上一个节点,所以需要保存上一个节点的位置,所以需要 plast
图解
在这里插入图片描述
2 同时,如果原来链表是空,就不能再进行删除
3 如果只有一个元素,头结点就需要置空
程序

void SLTPopBack(SLTHead** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLTNode* pcur = *pphead;
	SLTNode* plast = *pphead;
	while(pcur->next != NULL)
	{
		pcur = pcur->next;
		plast = pcur;
	}
	//判断是否只剩下一个节点
	if(pcur == plast)
	{
		free(pcur);
		*pphead = NULL;
	}
	else
	{
		free(pcur);
		plast->next = NULL;
	}
}

寻找链表元素与打印链表

第1.1个问题寻找链表元素需要做什么?
需要遍历,然后匹配,返回对应的位置;遍历过程参考尾插的过程
程序:

SLTNode* SLTSearch(SLTHead* phead,Datatype x)
{
	SLTNode* pcur = phead;
	while(pcur != NULL)
	{
		if(pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

第2.1个问题,打印的步骤是什么?
还是遍历,然后一个个打印出来

void SLTPrint(SLTHead* phead)
{
	SLTNode* pcur = phead;
	while(pcur != NULL)
	{
		printf("%d->",pcur->data);
		pcur = pcur->next;
	}
	printf("NULL");
}

第2.2 个问题
传参为何不用传 SLTNode** ?
因为
在Print中我们只是范围对应的结构体,使用结构体的地址就可以(只是没必要用指针的地址,但是是可以用的)
在Search中我们也没有对结构体的地址进行修改,只是得到对应地址的值,因此也只需要传结构体的地址

在 某位置后插入删除

第1.1个问题
为什么要先说在 pos(位置的代称) 后插入呢?
单链表的特性就是,可以通过上一个节点的 next 寻找到下一个节点,只要我们知道上一个节点的地址,就很容易能够找到下一个节点
第1.2个问题
如何找到上一个节点的位置?
记得在上一个小节,我们写了一个SLTSearch
我们可以通过这个函数找到有对应数据的节点

第1.3 个问题
步骤是什么

  1. 找到储存对应数据的节点的地址
  2. 生成新的节点
  3. 将节点链接到原来的链表

图例解释
在这里插入图片描述

第1.4个问题
链接的顺序是什么?
从图上可以看出,是 newnode 先链接到后面,然后再修改上一个节点的指向
因为是单链表,如果先将 pos -> next 指向newnode
pos 原来的后段就丢失了,导致出错

第1.5个问题
如果没有找到对应的节点怎么办?
因为定义中 Search 若没有找到对应的节点,由于这里是在某位置后插入,所以找不到就无法再插入了,就退出了

void SLTInseartBack(SLTNode* pos,SLTDatatype x)
{
	if(pos == NULL)
	{
		return ;
	}
	SLTNode* newnode = BuyNewNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
//这里是调用
void Test1()
{
	PList* phead;//假设这是一个已知链表的头结点的地址
	SLTNode* pos = SLTSearch(phead,x);
	SLTInseartBack(pos,x);
}

这里相似的,就会有在某个位置后删除
步骤是:

  1. 找到某个位置
  2. 找到pos ->next->next
  3. 将pos->next 删除
    图解
    在这里插入图片描述
    第2.1 个问题,如果没找到或者pos 没有元素呢?
    在这两种情况下都不能删除
    对应程序
void SLTDelBack(SLTNode* pos)
{
	if(pos == NULL || pos->next == NULL )
	{
		return ;
	}
	SLTNode* pfree = pos->next;
	pos->next = pos->next->next;
	free(pfree);
}

在某位置的插入删除

难点: 链表指针无法访问到前一个元素
首先第1.1个问题
我们想用这个函数做什么?
我们可以通过 Search 找到对应的 pos
我们想直接在 pos 的位置上插入或者删除元素,这是我们的目的
第1.2 个问题
这个函数实现的难点是什么?
因为是单链表,不能找到pos 前一个位置,所以需要遍历找到前一个位置
具体步骤

  1. 利用 search 找到 pos
  2. 利用遍历找到pos 前一个元素的位置
  3. 在前一个位置的后面插入删除
    图例解析
    在这里插入图片描述

第1.3个问题
如果pos 是空是什么情况?
如果 pos 是空,那么就意味着是链表的尾部,这就说明是尾插和尾删

第1.4个问题
如果一开始就没有元素呢?
一开始没有元素就需要改变头结点指针的参数
所以传参需要传头结点指针的地址
相当于空链表的尾插

第1.5个问题
如果 pos指向的是第一个元素呢?
我们需要从让新元素排到第一个
这个时候情况就变成了头插

所以对应的程序是

void SLTInsert(SLTNode** pphead,SLTNode* pos,Datatype x)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	if(pcur == NULL || pos == NULL)
	{
		SLTPushBack(pphead,x);
		return ;
	}
	
	if (pcur == pos)
	{
		SLTPushFront(pphead,x);
		return;
	}

	SLTNode* newnode = BuyNewNode(x);
	SLTNode* newnode = BuyNewNode(x);
	SLTNode* pnext = pcur->next;
	while(pnext != pos)
	{
		pcur = pnext;
		pnext = pnext->next;
	}
	newnode->next = pnext;
	pcur->next = newnode;
	
}

相对应的也有对应位置的删除
什么步骤呢?

  1. 找到对应的位置
  2. 利用遍历找到pos 上一个节点
  3. 删除pos的元素

问题2.1
尾删是不是一种特殊情况呢?
这个时候尾删, pos 就必须指向一个确定的位置,和普通情况一样,因此不用特殊处理
即使是只剩下一个节点,也可以当做头删来处理

图例解析
在这里插入图片描述

void SLTErase(SLTNode** pphead,SLTNode* pos)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	if(pos == NULL)
	{
		return ;
	}

	if(pos == pcur)
	{
		SLTPopFront(pphead);
		return;
	}
	SLTNode* pre = pcur;
	while(pcur != pos)
	{
		pre = pcur;
		pcur = pcur->next;
	}
	pre->next = pcur->next;
	free(pcur);
	
}

注意:
有一个值得注意的地方
就是 每次 pos 在 位置删除后,这个pos 就不能用了,要在函数外置为 NULL

销毁链表

销毁链表的步骤是?
遍历然后一个个销毁
但是每次要把将要销毁的元素的后一个元素保存起来

图例解析
在这里插入图片描述
程序实现

void SLTDestroy(SLTNode* pphead)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	SLTNode* pre = *pphead;
	while(pcur != NULL)
	{
		pre = pcur;
		pcur = pcur->next;
		free(pre);
	}
	*pphead = NULL;
}

希望大家看完,能够有所收获
如果有错误,请指出我一定虚心改正
动动小手点赞
鼓励我输出更加优质的内容

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

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

相关文章

链表(1)

我们以前学过的线性数据结构底层原理都是依托静态数组来实现的&#xff0c;今天我们讲学习一个最简单的动态数据结构---->链表&#xff01; 掌握链表有助于学习更加复杂的数据结构&#xff0c;例如&#xff1a;二叉树、trie 链表的优点是不需要处理固定的问题&#xff0c;…

mavon-editor的使用

vue3vitets下安装mavon-editor 3.0.0-beta版本&#xff0c;效果如下&#xff1a; 安装 //引入样式 import mavon-editor/dist/css/index.css; import mavonEditor from mavon-editor; app.use(router).use(mavonEditor).mount(#app);<template><div class"rich…

zabbix主动监控和被动监控

目录 一、环境准备 1、搭建zabbix基础环境 二、主动监控与被动监控介绍 三、设置客户端为主动监控 1、给web2主机安装zabbix_agent 2、修改主动监控配置 四、设置zabbix管理端主动监控 1、克隆模板 2、给目标主机绑定主动监控模板 3、查看主动监控的数据 一、环境准备…

【HIT-OSLAB-实验中的碎碎念】

文章目录应该养成的好习惯删除 替换 修改 内容时 记得留备份遇到问题要通过文字 图片 等多种途径去记录不同的项目应该在不同的文件夹进行处理代码文档 记得添加一些注释用于说明功能多输出有区别度的提示信息s找bug 先定位错误 再改当一份代码有不同版本的时候 记得说明每份代…

109376-05-8,Boc-QRR-AMC, Hepsin substrate

Boc-QRR-AMC是跨膜丝氨酸蛋白酶hepsin的底物&#xff0c;也用于测定酿酒酵母中的可辛(Kex2内蛋白酶)。Boc-QRR-AMC的库存解决方案最好在DMSO中准备。 编号: 187545中文名称: Hepsin substrate&#xff1a;Boc-Gln-Arg-Arg-7-氨基-4-甲基香豆素英文名: Boc-Gln-Arg-Arg-AMCCAS号…

全球No.1集装箱人工智能企业CIMCAI中集飞瞳,集装箱信息识别铅封号识别API免费,集装箱识别率99.98%高泛化性,全球两千+企业用户使用

全球No.1集装箱人工智能企业CIMCAI中集飞瞳&#xff0c;先进人工智能AI科技打造飞瞳引擎™AI集装箱检测云服务&#xff0c;集装箱信息识别铅封号识别API免费&#xff0c;集装箱识别率99.98%高泛化性&#xff0c;全球两千企业用户使用。CIMCAI中集飞瞳成熟港航人工智能核心技术及…

3年功能测试拿8K,被刚入职的应届生反超,其实你在假装努力

最近朋友给我分享了一个他公司发生的事 大概的内容呢&#xff1a;公司一位工作3年的测试工资还没有新人高&#xff0c;对此怨气不小&#xff0c;她来公司辛辛苦苦三年&#xff0c;三年内迟到次数都不超过5次&#xff0c;每天都是按时上下班&#xff0c;工作也按量完成&#xf…

PyQT6关联信号槽 (六) 百篇文章学PyQT6

本文章是百篇文章学PyQT6的第六篇&#xff0c;本文讲述如何使用PySide创建UI界面&#xff0c;并且关联入PyCharm 新建的项目中成功运行第一个PyQT程序&#xff0c;并且使用 信号槽 connect 到函数&#xff0c;在写博客和学习的过程中会遇到很多问题&#xff0c;例如&#xff1a…

Python实现点选验证码识别, B站模拟登陆

话不多说&#xff0c;今天就分享一下如何用Python实现点选验证码识别&#xff0c;小破站模拟登陆 开发环境 Python 3.8Pycharm 2021.2谷歌浏览器谷歌驱动 模块使用 selenium >>> pip install selenium3.141.0 指定版本安装time打码平台 模块安装问题: -如果安装…

Java注解(Annotation)

一、什么是注解 个人理解&#xff0c;注解就是代码中的特殊标记&#xff0c;这些标记可以在编译、类加载、运行时被读取&#xff0c;从而做相对应的处理。 注解跟注释很像&#xff0c;区别是注释是给人看的&#xff1b;而注解是给程序看的&#xff0c;它可以被编译器读取。 …

ERP软件定价策略与模型设计

ERP软件定价(价格)的高低是ERP厂商整体竞争力强弱的一个重要指针&#xff0c;也是影响客户购买行为的重要因素。客户购买某一ERP软件&#xff0c;总是面临不同的ERP厂商﹑不同渠道的多种选择&#xff0c;ERP软件价格往往成了除软件功能﹑售后服务态度、实施水平等因素外&#x…

web前端-Ajax基础学习

web前端-Ajax基础学习1. Ajax基础描述1.1 URL地址的概念1.2 客户端和服务器的通信过程1.3 Ajax1.3.1 $.get()函数1.3.2 $.post()1.3.3 $.ajax()1.4 接口1.4.1 GET、POST方式请求的过程1.4.2 接口文档2. form表单与模版引擎2.1 表单的基本介绍2.2 form表单同步提交的缺点2.3 通过…

stm32 笔记 外部中断以及HAL库应用

外部中断 由外部设备发起的中断请求&#xff0c;会使得设备暂停当前的主程序&#xff0c;保存标志位并把当前指令压入堆栈&#xff0c;转而去执行中断的子程序。执行完毕后再弹出执行堆栈&#xff0c;恢复标志位&#xff0c;继续执行主程序。 STM32 的外部中断线 STM32的每个…

嵌入式 C语言/C++ 常见笔试、面试题 难疑点汇总(经典100道)

#pragma comment。将一个注释记录放置到对象文件或可执行文件中。 #pragma pack。用来改变编译器的字节对齐方式。 #pragma code_seg。它能够设置程序中的函数在obj文件中所在的代码段。如果未指定参数&#xff0c;函数将放置在默认代码段.text中 #pragma once。保证所在文件只…

Pytest接口测试框架实战项目搭建(三)

一、前言 前面相当于已经讲完整体框架搭建了&#xff0c;本篇主要讲述在实际业务系统的接口请求中&#xff0c;如何运用好该接口自动化测试框架。 二、步骤演示 1、在conf/api_path.py新增需要测试的接口&#xff0c;标黄底色为新加 存放测试接口仅这一个文件就行&#xff0c…

吉时利2604B系列数字源表,双通道,3A直流/10A脉冲

作为2600B系列源表SMU系列产品的一部分&#xff0c;2602B源表SMU是全新改良版双通道SMU&#xff0c;具有紧密集成的4象限设计&#xff0c;能同步源和测量电压/电流以提高研发到自动生产测试等应用的生产率。除保留了2602A的全部产品特点外&#xff0c;2602B还具有6位半分辨率、…

Android 面试题收集:Handler+Binder+Activity+时间分发机制+View绘制流程+……等

一、Handler相关知识 一个线程只有一个Looper&#xff0c;一个Messagequeue&#xff0c;可以创建多个handler。 1、Handler与Looper的关联是怎样的? 实例化 Handler 的时候 Handler 会去检查当前线程的 Looper 是否存在&#xff0c;如果不存在则会报异常&#xff0c;也就是…

关于TreeView的简单使用(Qt6.4.1)

前言 TreeView是在Qt6.3中加入的&#xff0c;弥补了Qt中无官方树图。笔者上手尝试了下&#xff0c;虽然有点麻烦&#xff0c;但官方也做了不少简化。 本次教程&#xff0c;笔者创建一个简单的示例&#xff0c;以帮助读者使用TreeView。 一、创建模型类 当前模型需要使用C定义…

人工智能概况笔记

文章目录一、人的智能与人工智能二、人工智能的发展历程三、人工智能的主要应用四、人工智能的伦理思考五、神经网络与深度学习六、国内外人工智能动向一、人的智能与人工智能 智能&#xff1a;基于推理的学习、理解和做出判断或意见的能力 人的智能&#xff08;Human Intell…

【数据结构-树】哈夫曼树及其应用

文章目录1 哈夫曼树的构造2 哈夫曼树的应用——哈夫曼编码3 相关例子1 哈夫曼树的构造 将 n 个结点作为 n 棵仅含一个节点的二叉树&#xff0c;构成森林 F在 F 中选取两棵权值最小的二叉树&#xff0c;作为新结点的左右子树&#xff0c;并将新结点的权值置为左、右子树的根结点…