单链表总结提升

news2024/11/28 8:26:06

这篇博客讲解数据结构中的单链表,包括单链表的基础知识和我对链表实现的总结理解,希望可以帮助到正在学习的小伙伴,也希望得到小伙伴们的关注和支持哦~

目录

1.单链表的概念

1.2顺序表和链表的对比

顺序表:

链表: 

链表相对于顺序表:

2.单链表的结构

3.单链表的实现

总结单链表函数书写要点:

一、关于是否需要分支: 

二、关于删除操作:

三、关于为什么用二级指针 


1.单链表的概念

 在之前顺序表的博客中讲解过线性表

  • 线性表在逻辑上是线性结构,也就说是连续的一条直线。
  • 但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

顺序表在逻辑结构和物理结构上都是连续的(类似数组),而今天要说的链表在物理结构上是非连续的!

所以,链表到底是什么呢?它和顺序表到底有哪些不同呢?

1.2顺序表和链表的对比

顺序表:

实质上就是对数组的操作,相对于静态顺序表来说,动态顺序表可以更加灵活地申请使用空间,不过,在顺序表使用的后期,开辟的内存成倍增加,如果再次加入的数据不多,还是会导致空间的浪费,而且在每次非尾部插入和删除时,总要对数组元素进行大量移动,效率还是没有那么高。

再次看一下顺序表的结构:

typedef int SLDateType;
typedef struct SeqList {
	SLDateType* a;
	int size;//有效数据个数
	int capacity;//当前顺序表容量
}SL;

顺序表缺点小总结:

  1. 后期内存开辟成倍增加
  2. 插入删除元素对顺序表移动元素较多

 从中我们也可以猜测出链表的功能,开辟内存不会成倍增加且插入删除不必移动过多元素

链表: 

实质上是指针链接,就好比如图所示的火车:

链表的结构跟火车车厢相似,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只 需要将火车里的某节车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。车厢是独立存在的,且每节车厢都有车门。想象一下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带一把钥匙的情况下如何从车头走到车尾? 最简单的做法:每节车厢里都放一把下一节车厢的钥匙。 这里的钥匙就是指针。

链表相对于顺序表:

  1. 将数组这一整体变为一个一个的链表节点,从对整体开辟内存变为对单个节点开辟内存
  2. 将顺序表用于计数的size和capacity去除,不再有越界的问题,取而代之的是链接下一个节点的指针
  3. 链表缺点在于无法通过下标访问,只能从头一个一个去访问,原因是一个一个申请的内存空间可能连续也可能不连续
  4. 顺序表和链表都是基于结构体实现

2.单链表的结构

ps:节点和结点表示一个意思 

画出单链表的结构大致如下:

  • 所有的链表节点都使用链表结构体指针去访问,一个链表节点包含数据区和指针区,数据区用于存放数据;指针区用于存放下一个节点的地址 
  • plist相当于火车头,出于方便,习惯叫它头结点,不过这种叫法其实不符合逻辑,也是结构体指针类型,单链表也可以没有plist
  • 还有一种链表,叫带头链表,这种链表的头节点数据区没有值,这个头节点叫做哨兵位,区分一下这两种链表

单链表结构体用代码书写就是:

typedef int SLTDateType;
typedef struct SListNode {
	SLTDateType val;//存放有效值
	struct SListNode* next;//指向下一个节点
}SLTNode;

SList 代表 single list 

3.单链表的实现

 使用单链表实现数据的增删查改

SList.h存放链表结构体及相关函数声明:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SLTDateType;
typedef struct SListNode {
	SLTDateType val;
	struct SListNode* next;
}SLTNode;
//头插
void SLTPushFront(SLTNode** pphead, SLTDateType x);

//尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x);

//头删
void SLTPopFront(SLTNode** pphead);

//尾删
void SLTPopBack(SLTNode** pphead);

//申请内存
SLTNode* SLTBuyNode(SLTDateType x);
//打印链表
void SLTPrint(SLTNode* phead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDateType x);

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDateType x);

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos);

//销毁链表
void SLTDestroy(SLTNode** pphead);

 SList.c存放相关函数定义

#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//申请内存
SLTNode* SLTBuyNode(SLTDateType x)
{
	SLTNode* p = (SLTNode*)malloc(sizeof(SLTNode));
	if (p == NULL)
	{
		perror("fail malloc");
		exit(1);
	}
	p->val = x;
	p->next = NULL;
	return p;
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDateType x)
{
	assert(pphead);
	SLTNode* pcur = SLTBuyNode(x);//不用找尾,不用对原先链表解引用,故不用考虑链表为空的情况
	pcur->next = *pphead;
	*pphead = pcur;

}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x)
{
	assert(pphead);
	SLTNode* ptail=*pphead;
	//链表为空
	if (*pphead == NULL)//如果链表为空,则找尾时不能对空指针解引用,故需要分支
	{
		*pphead = SLTBuyNode(x);
	}
	else
	{
		//找尾
		while (ptail->next != NULL)
		{
			ptail = ptail->next;
		}
		//接入
		ptail->next = SLTBuyNode(x);
	}

}
//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead&&*pphead);
	//只有一个节点和有多个节点可以使用相同方法//需要头结点后的位置的地址,若只有一个节点,其后面有位置,故不用分支
		SLTNode* pcur = *pphead;
		*pphead = (*pphead)->next;
		free(pcur);
		pcur = NULL;	
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* ptail = *pphead;
	SLTNode* prev = *pphead;
	if (ptail->next == NULL)//只有一个节点(ptail prev未错开)//需要尾节点前一位的地址,若尾节点为头节点,头结点前无位置,故需要分支
	{
		free(*pphead);
		*pphead = NULL;
	}else//有多个节点(ptail prev错开)
	{
		//找尾
		while (ptail->next != NULL)
		{
			prev = ptail;

			ptail = ptail->next;
		}

		free(ptail);
		ptail = NULL;
		prev->next = NULL;

	}
}
//打印链表
void SLTPrint(SLTNode* phead)
{
	//assert(phead);
	while (phead != NULL)
	{
		printf("%d->", phead->val);
		phead = phead->next;
	}
	printf("NULL\n");
}
//查找
SLTNode* SLTFind(SLTNode* phead,SLTDateType x)
{
	SLTNode* pcur = phead;
	while (phead)
	{
		if (phead->val == x)
		{
			return phead;
		}
		phead = phead->next;
	}
	return NULL;
}

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* pcur = *pphead;
	//第一个位置为指定位置
	if (pcur == pos)//需要指定位置之前的位置和指定位置的地址,头节点之前无位置,故需要分支
	{
		SLTPushFront(pphead, x);
	}
	//其他位置
	else
	{
		while (pcur->next != pos)
		{
			pcur = pcur->next;
		}
		//插入
		SLTNode* pin = SLTBuyNode(x);
		pin->next = pos;
		pcur->next = pin;
	}

}

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* pcur = *pphead;
	//找到指定位置
	while (pcur != pos)//需要指定位置和指定位置之后位置的地址,尾节点后有位置,故不需要分支
	{
		pcur = pcur->next;
	}
	//插入(最后节点后插入与其他位置相同)
	SLTNode* pin = SLTBuyNode(x);
	pin->next = pcur->next;
	pcur->next = pin;
}

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* pcur = *pphead;

	//删除头结点
	if (pcur == pos)//需要pos前和pos后的位置,若pos为头结点,头结点前无位置,故需要分支
	{
		SLTPopFront(pphead);
	}else//非头结点
	{
		while (pcur->next != pos)
		{

			pcur = pcur->next;
		}

		pcur->next = pos->next;//pos相当于尾删里面的prev,故不用再创建结构体指针来保存前一个位置的next
		free(pos);
		pos = NULL;
	}
}

//删除pos之后的节点
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos && pos->next);
	SLTNode* pcur = *pphead;
	while (pcur!=pos)//需要pos和pos之后的之后的地址,若pos为尾节点,尾节点后有位置,故不需要分支
	{
		pcur = pcur->next;
	}
	SLTNode* pout = pos->next;//pos和pcur指向相同,pcur发生改变导致pos发生改变,保存pos地址用于删除
	pcur->next = pos->next->next;
	free(pout);
	pout = NULL;
}

//销毁链表
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* pcur;
	while (*pphead)
	{
		pcur = *pphead;
		*pphead = (*pphead)->next;
		free(pcur);
		pcur = NULL;
	}
}

总结单链表函数书写要点:

一、关于是否需要分支: 

1.传入链表可能为空的函数: 关键要清楚是否需要某个节点的next(也就是是否对需要某节点指针解引用)

  • 对于头插尾插:头插不需要原链表头结点的next尾插需要原链表尾节点的next用于链接,故尾插需要考虑链表为空的情况(不能对空指针解引用)。尾插需要分支,头插不用分支。 

2.传入链表不能为空的函数(讨论只有一个节点的情况):关键要确定需要找的节点是否存在(节点为NULL也是存在,不存在指找不到其地址)

  • 对于头删尾删:头删需要头结点后面的节点,当链表只有一个节点时,头结点后面的节点存在,故头删不用分支尾删需要尾节点前面的节点,当链表只有一个节点时,尾节点前面的节点不存在,故尾删需要分支
  • 对于任意位置插入:pos之前插入需要pos和pos前面的节点,当链表只有一个节点时,pos前面节点不存在,故前插需要分支pos之后插入需要pos和pos后面的节点,当链表只有一个节点时,pos后面节点存在,故后插不用分支
  • 对于任意位置删除:pos位置删除需要pos前面和pos后面的节点,当链表只有一个节点时,pos前面节点不存在,故pos删除需要分支pos之后删除需要pos和pos后面的后面的节点,当链表只有一个节点时,接收链表时就要断言报错,因为无法删除一个不存在的节点,故pos后删不用分支

总体来说,相对于参考节点,向前看的需要分支讨论,向后看的不需要分支讨论 

  • 向前看的(要分支的)都是用pcur->next 与某节点比较作为循环判断条件
  • 向后看的(不要分支的)都是用pcur与某节点比较作为循环判断条件

二、关于删除操作:

关键在于确定删除操作中链接前后节点所使用的指针是否使被删的节点指针指向发生改变 

  • 对于头删:需要头结点和头结点后的节点,头结点为被删节点,链接前后节点时,头指针指向发生了改变,故需要指针保存原来的头结点,进行后续删除。
  • 对于尾删:需要尾节点和尾节点前的节点,尾节点为被删节点,链接前后节点时,尾指针指向没有发生改变,故直接删除
  • 对于删pos节点:需要pos前和pos后的节点,pos为被删节点,链接前后节点时,pos指针指向没有发生改变,故直接删除
  • 对于删pos后的节点:需要pos和pos后面的后面的节点,pos->next为被删节点,链接前后节点时,pos指针指向发生改变,会导致pos->next指针指向发生改变,故需要指针保存原来的pos->next,进行后续删除。

也就是说,需要使用的指针与被删除的指针有关系时,需要指针保存节点(认为前一个节点与被删节点无关,因为被删节点无法向前找节点)  

  • 向前看的(有分支的)不需要指针保存被删节点
  • 向后看的(没有分支的)需要指针保存被删节点

三、关于为什么用二级指针 

首先:函数传参有传值,传址两种 

值传递不改变实参的值,按址传递在函数中通过解引用能改变实参的值

对于想要传入指针的函数(下面说两种情况,以结构体类型为例): 

传入一级指针,用一级指针接收:

 相当于传了一个结构体的址,结构体指针指向。此时我们可以通过解引用改变结构体的内容,但是不能改变指针的指向(指向该结构体的地址)        (ps:想想之前写题:数组传参用一级指针来接收 ,函数里面改变的是数组各元素的值而不是数组的地址)

传入一级指针的地址,用二级指针接收:

相当于传了结构体指针指向。此时我们可以通过解引用改变结构体指针的指向

  • 头插,头删操作明确要改变头结点的指针的指向,用到二级指针
  • 尾删,删除pos节点,pos前插入节点时,遇到一个节点的情况也要改变头指针指向,也要用到二级指针
  • 尾插操作是遇到空链表时,需要改变头指针指向,还要用到二级指针
  • 销毁操作需要将指针指向空,都要用到二级指针

所以,编写单链表的函数的时候,我们使用二级指针 

 --------------------------------------------------------------------------------------------------------------------------------

好啦,单链表的讲解就到这里啦,上面的总结部分真的是博主认为的精髓!!!(希望不是自以为是),看完的小伙伴是否能够留下你们的收藏关注呢(比心)(比心)

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

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

相关文章

【牛客SQL快速入门】SQL基础(三)

一、条件函数 IF 条件函数 IF函数是最常用到的条件函数&#xff0c;写法为 if(xn,a,b)&#xff0c;xn代表判断条件&#xff0c;如果xn时&#xff0c;那么结果返回a&#xff0c;否则返回b。 -- 把非北京大学的用户统一归为其他大学 Select device_id,if(university ‘北京大…

C++设计模式:享元模式(十一)

1、定义与动机 概述&#xff1a;享元模式和单例模式一样&#xff0c;都是为了解决程序的性能问题。面向对象很好地解决了"抽象"的问题&#xff0c;但是必不可免得要付出一定的代价。对于通常情况来讲&#xff0c;面向对象的成本大豆可以忽略不计。但是某些情况&#…

T2.数据库原理

2.1 关系模型概述 1.域 域&#xff1a; 一组有相同数据类型的值得集合 2.笛卡尔积 笛卡尔积&#xff1a; 设任意的N个域D1,D2,…,Dn。 3、关系的定义和性质 &#xff08;1&#xff09;关系的数学定义&#xff1a; 在笛卡儿积中取出有实际意义的元组来构造关系。 关系也是…

R语言绘制一次和二次相关性热图

在数据探索的过程中&#xff0c;我们往往会对数据与数据的相关性进行分析&#xff0c;例如我们常用的corrplot包&#xff0c;或者psych包中的corr.test函数&#xff0c;对两两变量间的相关性进行分析。我们常常会看到这样的相关性热图&#xff1a; 但有时变量间的关系并非线性…

new mars3d.thing.Sightline({ 实现航线穿透山体/模型部分的虚线效果且不随视角变化而变化

摘要&#xff1a;new mars3d.thing.Sightline({ 实现航线穿透山体/模型部分的虚线效果且不随视角变化而变化&#xff0c;避免depthFailMaterial材质随着视角变化遮挡部分也会虚线效果。 前置说明&#xff1a; 1.不可视区域颜色hiddenColor支持设置材质&#xff0c;实现穿过山…

【spring】@PostConstruct和@PreDestroy注解学习

PostConstruct和PreDestroy介绍 PostConstruct和PreDestroy是Java EE&#xff08;Enterprise Edition&#xff09;和Java SE&#xff08;Standard Edition&#xff09;中的注解&#xff0c;它们用于指定一个bean在其生命周期的特定点应该执行的方法。 这两个注解是JSR250规范中…

【高端电流检测IC储能产品应用方案】耐压28V侧轨的电流检测芯片FP130A 应用于电脑电源,开关电源以及多口快充充电器,户外移动电源,适配器,电池充电器等

电流检测技术常用于高压短路保护、电机控制、DC/DC换流器、系统功耗管理、二次电池的电流管理、蓄电池管理等电流侦测等场景。对于大多数应用而言&#xff0c;都是间接测量电阻两端的跨压差来获取待测电流。 如下面的高端电流检测芯片FP130A&#xff0c;丝印是FC915。电路原理图…

MySQL 全文检索

不是所有的数据表都支持全文检索 MySQL支持多种底层数据库引擎&#xff0c;但是并非所有的引擎支持全文检索 &#xff0c;目前最常用引擎是是MyISAM和InnoDB&#xff1b;前者支持全文检索&#xff0c;后者不支持。 booolean模式操作符 操作符含义必须有-必须不包含>包含对应…

Unity 中画线

前言&#xff1a; 在Unity项目中&#xff0c;调试和可视化是开发过程中不可或缺的部分。其中&#xff0c;绘制线条是一种常见的手段&#xff0c;可以用于在Scene场景和Game视图中进行调试和展示。本篇博客将为你介绍多种不同的绘制线条方法&#xff0c;帮助你轻松应对各种调试…

rust使用print控制台打印输出五颜六色的彩色红色字体

想要在控制台打印输出彩色的字体&#xff0c;可以使用一些已经封装好的依赖库&#xff0c;比如ansi_term这个依赖库&#xff0c;官方依赖库地址&#xff1a;https://crates.io/crates/ansi_term 安装依赖&#xff1a; cargo add ansi_term 或者在Cargo.toml文件中加入&#…

HWOD:在含有空格的字符串中统计大写字母的个数

一、知识点 回车键的ASCII码是10 二、题目 1、描述 找出给定字符串中大写字符(即A-Z)的个数。 数据范围&#xff1a;字符串长度&#xff1a;1≤∣s∣≤250 字符串中可能包含空格或其他字符 2、输入 对于每组样例&#xff0c;输入一行&#xff0c;代表待统计的字符串 …

DCDC 5V2A电源升压芯片FP6276BXR-G1 FP6298XR-G1

一、FP6276BXR-G1 3.7v升5V2A同步升压输入电压:2.4V-4.5V FP6276B是一个具有PWM/PSM控制的电流模式增压直流-直流转换器。它的PWM电路内置40mΩ高侧开关和40mΩ低侧开关使该调节器高高效。内部补偿网络还将外部组件计数最小化到只有6个。一个内部的0.6V电压被连接到误差放大器…

玩机进阶教程------手机定制机 定制系统 解除系统安装软件限制的一些步骤解析

定制机 在于各工作室与商家合作定制rom中有一些定制机。限制用户私自安装第三方软件。或者限制解锁 。无法如正常机登陆账号等等。定制机一般用于固定行业或者一些部门。专机专用。例如很多巴枪扫描机型等等。或者一些小牌机型。对于没有官方包的机型首先要导出各个分区来制作…

Java快速入门系列-8(Web开发基础)

第8章 Web开发基础 8.1 Servlet与JSP8.1.1 Servlet简介8.1.2 JSP简介与使用8.1.3 Servlet与JSP协作8.2 Web服务器与Tomcat8.2.1 安装与配置Tomcat8.2.2 配置与管理Web应用8.3 MVC设计模式与Java Web应用8.3.1 MVC原理8.3.2 MVC在Java Web中的应用8.4 RESTful API设计与实现8.4.…

学习R语言第二天

R语言可以做什么 1.数据分析 R语言如何使用 1. 请看我的操作方式 2. 如何获取当前路径 -- 获取当前路径 > getwd() [1] "E:/R/RWorkSpace/day01" -- 修改当前路径 > setwd(dir "E:/R") > getwd() [1] "E:/R" 3.查看当下数据值的信…

计算机毕业设计 工厂车间设备管理系统php+vue.js

该系统是以PHP为主要开发语言&#xff0c;集合MySQL数据库技术构建基于Web的以信息为目标的&#xff0c;动态的交互平台 PHP与Apache&#xff0c;MySQL的组合虽然有着诸多优势&#xff0c;但三种出自不同作者的开源软件要能很好的配合工作&#xff0c;需要很多手工配置&#x…

源码解析HashMap的put方法

前言 HashMap 基于哈希表的 Map 接口实现&#xff0c;其中的值是以 key-value 存储形式存在&#xff0c;即主要用来存放键值对。它的 key、value 都可以为 null&#xff0c;此外&#xff0c;HashMap 中的映射不是有序的。那么本篇文章将从源码的角度来很详细地讲解HashMap中pu…

5G智慧水利数字孪生可视化平台,推进水利行业数字化转型

5G智慧水利数字孪生可视化平台&#xff0c;推进水利行业数字化转型。随着5G技术的快速发展&#xff0c;越来越多的行业开始探索数字化转型的道路。水利行业作为国民经济的重要支柱&#xff0c;也面临着数字化转型的迫切需求。5G智慧水利数字孪生可视化平台作为水利行业数字化转…

微服务-7 Docker

一、镜像、容器、仓库 容器是镜像的实例&#xff0c;仓库中存储着镜像。 二、镜像的操作 三、容器的操作 创建容器停止容器&#xff0c;查看后发现没有了(docker ps 默认只展示没有停止的) docker ps -a (可以展示运行中和停止的镜像)删除容器&#xff1a;(docker rm 不能删除…

LangChain-18 Caching 将回答内容进行缓存 可在内存中或数据库中持久化缓存

背景描述 可以将问答的内容缓存起来&#xff0c;如果是相同的问题&#xff0c;那么将会直接把答案返回去&#xff0c;可以节约费用和计算。 安装依赖 pip install -qU langchain-core langchain-openai编写代码 我们可以通过 InMemoryCache 进行内存缓存 或者 SQLiteCache …