数据结构学习分享之单链表详解

news2024/12/23 21:30:54

数据结构第三课

  • 1. 前言
  • 2. 链表的概念以及结构
  • 3. 链表的分类
  • 4.链表的实现
    • 4.1 初始化结构
    • 4.2 尾插函数
    • 4.3 尾删函数
    • 4.4 头插函数
    • 4.5 头删函数
    • 4.6 开辟新节点
    • 4.7 销毁链表
  • 5. 单链表OJ题目
  • 6. 顺序表和链表的区别
  • 7. 总结


1. 前言

    💓博主CSDN:杭电码农-NEO💓🎉🎉🎉

    ⏩专栏分类:数据结构学习分享(持续更新中🫵)⏪🎉🎉🎉

  让我们紧接上一章顺序表的概念,引出链表,我们说顺序表每次增容需要申请新的空间,会产生很多空间碎片,代价比较高,并且我们扩容一般是扩两倍,还是会有一些空间被我们浪费掉. 所以我们基于顺序表的缺点,引出了链表的概念


2. 链表的概念以及结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。我们光听这个概念可能有一点抽象,所以我们以画图的形式给大家说明一下;

在这里插入图片描述


我们画的图是形象图,但是实际上我们链表没有箭头这一说法,箭头只是帮助大家理解它的结构,其实它的真实存储结构应该是这样的: 🔑🔑

在这里插入图片描述注意:

  • 在上图中我们可以发现链式结构在逻辑上是连续的(即一个节点链接着一个节点),但是在物理上不一定连续(即地址不连续,节点1为A0,节点2为B0) 🔥🔥

  • 实际存储中的节点是从堆上申请出来的,在堆上申请的空间是按照一定的策略来分配的,两次手球的空间可能连续也可能不连续 🔥🔥


3. 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构

  1. 单向或者双向:
    在这里插入图片描述

  1. 带头或者不带头(也叫做哨兵位):
    在这里插入图片描述

  1. 循环或非循环:

在这里插入图片描述


虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

在这里插入图片描述

  • 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多💬

  • 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了💬。

我们本节要介绍的就是无头单向非循环链表 🎉🎉🎉

4.链表的实现

4.1 初始化结构

和顺序表一样,在我们实现增删查改等功能之前,我们要先在.h文件中包含常见的头文件并且定义一个结构体后将它重命名:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDateType;//想存储字符型就把int改成char
typedef struct SlistNode
{
	SLTDateType data;//链表节点中存储的数据
	struct SlistNode* next;//节点中存储的下一个节点的地址
}SLTNode;

然后在在我们的test.c文件和Slist.c文件中包含Slist.h.不同于顺序表的是我们这里在test.c文件中定义一个plist结构体指针就可以直接开始使用了.我们接下来先在.h文件中将我们所有的函数都声明一遍:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDateType;
typedef struct SlistNode
{
	SLTDateType data;
	struct SlistNode* next;
}SLTNode;

void SListPrint(SLTNode** phead);//打印链表元素

SLTNode* BuyListNode(SLTDateType x);//开辟空间

void SListPushBack(SLTNode** phead, SLTDateType x);//尾插

void SListPopBack(SLTNode** phead);//尾删

void SListPushFront(SLTNode** phead, SLTDateType x);//头插

void SListPopFront(SLTNode** phead);//头删

SLTNode* SListFind(SLTNode* phead, SLTDateType x);//查找

void SListInsert(SLTNode** phead, SLTNode* pos, SLTDateType x);//在pos位置前插入数据

😄😄😄

我们发现这里的增删查改都是传的二级指针,这是因为我们在test.c中定义结构体变量时定义的是一个结构体指针指向我们链表的第一个节点,我们在进行增删查改的过程中可以会改变这个结构体指针的指向(比如头插后,我们的结构体指针指向新的链表的头),所以我们需要传二级指针过去才能改变它的值.如果听到这儿你还不明白,可以先去我的码云看看所有的代码再慢慢理解gitee-杭电码农


4.2 尾插函数

既然是要插入数据,那么与数组不同的是链表每插入一次数据就开辟一块与这个数据大小相同的空间,所以我们第一步要做的就是动态开辟空间:

void SListPushBack(SLTNode** phead, SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//定义一个新节点后动态开辟空间
	newnode->data = x;//将要插入的数据x赋值到节点的data上
	newnode->next = NULL;//将尾插后的节点的next置空.
}

当我们开辟好空间之后,我们得先找到尾,才能尾插,所以我们这里定义一个变量取遍历链表来找到尾:

void SListPushBack(SLTNode** phead, SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//定义一个新节点后动态开辟空间
	newnode->data = x;//将要插入的数据x赋值到节点的data上
	newnode->next = NULL;//将尾插后的节点的next置空.

    SLTNode* cur = *phead; //定义一个变量指向链表得头节点
    while (cur->next!=NULL)//当cur->next等于NULL时,此时cur就是最后一个节点
	   {
		  cur = cur->next;//cur不断往后遍历
	   }
	   cur->next = newnode;//这时cur已经等于最后一个节点后出了while循环,将最后一个节点与新节点链接起来
}

值得注意的是这里有一种特殊情况,就当整个链表没有存储数据时,我们得phead为空指针,cur也是空指针,然后我们使用了cur->next相当于对空指针解引用了,所以这个地方得特殊情况要特殊考虑,优化代码后为:

void SListPushBack(SLTNode** phead, SLTDateType x)
{
    assert(*phead);//确保链表不为空
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;
	if (*phead == NULL)
	{
		*phead = newnode;//当链表为空时直接将phead给给newnode
	}
	else
	{
	   SLTNode* cur = *phead; 
       while (cur->next)
	   {
		cur = cur->next;
	   }
	   cur->next = newnode;
	}
}


4.3 尾删函数

和尾插类型,先找到尾再把尾节点的空间给释放掉,然后将尾节点的前一个节点的next置空使它变成新的尾节点:

void SListPopBack(SLTNode** phead)
{
		SLTNode* cur = *phead;
		SLTNode* prev = NULL;//定义一个prev指向cur的前一个结点
		while (cur->next!=NULL)//当cur->next为空时就代表cur是尾节点了,此时prev是尾节点的前一个结点
		{
			prev = cur;//prev是cur结点的前面一个结点
			cur = cur->next;
		}
		prev->next = NULL;//将prev结点置空成为新的尾
		free(cur);//将要尾删的空间给释放掉
		cur = NULL;
}

还是和之前同一个问题,当我们链表中只有一个节点时,我们的prev还是NULL,后面讲prev->=NULL,也是对空指针解引用,也会遇见对空指针解引用的错误操作,所以这个地方我们优化代码:

void SListPopBack(SLTNode** phead)
{
	if ((*phead)->next == NULL)
	{
		free(*phead);//当链表中只有一个节点时,直接free掉就行
		*phead = NULL;
	}
	else
	{
		SLTNode* cur = *phead;
		SLTNode* prev = NULL;
		while (cur->next)
		{
			prev = cur;
			cur = cur->next;
		}
		prev->next = NULL;
		free(cur);
		cur = NULL;
	}
}


4.4 头插函数

有了前面尾插,尾删的基础,这里我们直接上代码:

void SListPushFront(SLTNode** phead, SLTDateType x)
{

	SLTNode* newhead = (SLTNode*)malloc(sizeof(SLTNode));//只要是插入数据就要开辟新空间
	newhead->next = *phead;
	newhead->data = x;
	*phead = newhead;//把新的头给phead
}


4.5 头删函数

这里头部的删除要考虑链表中是否还存在节点,并且链表中只有一个节点的情况也要拿出来特殊考虑

void SListPopFront(SLTNode** phead)
{
	assert(*phead);//确保链表不为空
	if ((*phead)->next == NULL)//链表只有一个节点
	{
		free(*phead);//直接释放掉空间后置空
		*phead = NULL;
	}
	else
	{
		SLTNode* newhead = (*phead)->next;//定义头节点的下一个节点,不然把头节点的空间释放掉后会找不到下一个节点
		free(*phead);
		*phead = newhead;//把phead给上新的头节点
	}
}


4.6 开辟新节点

我们发现,在操作链表时,只要涉及到插入数据就会用到开辟动态内存这一个步骤,所以我们可以把这个步骤单独拿出来,遇见头插尾插时可以在函数中调用这个开辟空间的函数:

SLTNode* BuyListNode(SLTDateType x)//返回一个节点的地址
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;//将插入的数据给上
	newnode->next = NULL;
	return newnode;//将开辟好后的空间返回
}

当我们写了这个函数过后,我们的头插和尾插就可以简化一点了🥳🥳🥳
比如我们的尾插可以改为:

void SListPushBack(SLTNode** phead, SLTDateType x)
{
    assert(*phead);//确保链表不为空
	SLTNode* newnode = BuyListNode(SLTDateType x);//定义一个节点来接受开辟的空间
	if (*phead == NULL)
	{
		*phead = newnode;//当链表为空时直接将phead给给newnode
	}
	else
	{
	   SLTNode* cur = *phead; 
       while (cur->next)
	   {
		cur = cur->next;
	   }
	   cur->next = newnode;
	}
}


4.7 销毁链表

当我们使用完链表后,需要销毁它里面的数据和空间:

void SListDestroy(SLTNode** phead)
{
	assert(phead);
	SLTNode* cur = *phead;
	while(cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*phead = NULL;
}


5. 单链表OJ题目

在我的专栏单链表面试题分享中其实就给大家分享了很多个单链表面试题的题解思路以及一些衍生问题的探讨,有兴趣的同学可以点击前面跳转.或者如果你想自己做一遍题不想先看答案的,我给大家把这些题的链接放出来 :

  ✍🏼✍🏼✍🏼

  • 删除链表中等于给定值 val 的所有节点。OJ题

  • 反转一个单链表.OJ题

  • 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个
    中间结点
    。OJ题

  • 输入一个链表,输出该链表中倒数第k个结点OJ题

  • 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成
    。OJ题

  • 链表的回文结构。OJ题

  • 输入两个链表,找出它们的第一个公共结点。OJ题

  • 给定一个链表,判断链表中是否有环。OJ题

  • 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULLOJ题

  • 给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
    要求返回这个链表的深度拷贝
    。OJ题

在我往期的博客当中有这些题目的画图详解,当你们做题时遇见问题可以跳转我的专栏单链表面试题分享

6. 顺序表和链表的区别

我们用一个表格来阐述:

不同点顺序表链表
存储空间上物理上一定连续逻辑上一定连续,物理上不一定
随机访问支持O(1)O(N)
任一位置插入或删除需要挪动元素,效率低只需要修改指针指向
插入动态顺序表,空间不够需扩容没有容量的概念
应用场景元素高效存储和快速访问频繁的任一位置插入或删除
缓存利用率

后期遇见既可以用顺序表作为结构也可以用链表作为结构的数据时,再详细讨论到底用哪个好



7. 总结

链表是为了弥补顺序表的缺陷而出现的一种数据结构,当然链表本身也有缺陷,它不能解决所有问题,所以我们后期还会学一些数据结构来完善我们对于结构的理解 有关于链表中最简单的结构:单链表的实现我们就讲到这里,如果有帮到你请点点赞吧.📝📝📝

💕 我的码云:gitee-杭电码农-NEO💕

🔎 下期预告:双向带头循环链表 🔍

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

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

相关文章

五年开发经验前端程序员,刚入职一个月就要离职,我来聊聊看法

最近有一个新来的同事&#xff0c;估计又要离职了吧。从他的工作经历来看&#xff0c;大概有5年的前端工作经验&#xff0c;但是头发看起来挺少的&#xff0c;不知道是工作加班导致的&#xff0c;看他的性格不太像是经常加班的。 他这个人就是我们公司人事面试的&#xff0c;虽…

操作系统——进程管理

0.关注博主有更多知识 操作系统入门知识合集 目录 0.关注博主有更多知识 4.1进程概念 4.1.1进程基本概念 思考题&#xff1a; 4.1.2进程状态 思考题&#xff1a; 4.1.3进程控制块PCB 4.2进程控制 思考题&#xff1a; 4.3线程 思考题&#xff1a; 4.4临界资源与临…

躺平减脂减重法补充篇——无需控制碳水摄入的有效方法,另推一种健康的运动和防止老年慢性病的方式...

本文此前已经连续发表了六篇相关文章&#xff0c;内容确实比较多&#xff0c;最近又做了一组实验&#xff0c;进食了大量的锅巴&#xff0c;看看是否会带来体重的增加&#xff0c;每天进食量都不少于200克锅巴&#xff0c;对&#xff0c;4两重&#xff0c;而且是在每天正常进食…

SAPUI5 之XML Views (视图) 笔记

文章目录 官网 Walkthrough学习-XML Views视图案例要求&#xff1a;我们把上面通过index.html body的展示放在XML中展示1.0.1 新增view文件夹1.0.2 在xml文件中新增一个Text 文本1.0.3 在index.js中实例化view视图1.0.4 执行刷新浏览器1.0.5 调试界面分析结果 官网 Walkthrough…

假期给朋友介绍如何学习java和找工作的建议?

Java学习 一、学习Java的建议1. 学习Java基础知识2. 学习Java框架3. 学习Java Web开发4. 学习Java数据库编程5. 学习Java工具6.学习Java中的多线程技术6. 练习编程 二、找工作的建议1. 准备好简历2. 寻找工作机会3. 准备面试4. 提高自己的技能5. 关注行业动态 学习Java和找工作…

第十九章 观察者模式

文章目录 前言普通方式解决问题CurrentConditions 显示当前天气情况WeatherData 管理第三方Clint 测试 一、观察者模式(Observer)原理完整代码SubjectObserverWeatherData implements SubjectCurrentConditions implements ObserverBaiduSite implements ObserverClint 前言 普…

《软件工程教程》(第2版) 主编:吴迪 马宏茹 丁万宁 第十章课后习题参考答案

第十章 面向对象设计 课后习题参考答案 一、单项选择题 &#xff08;1&#xff09;A &#xff08;2&#xff09;B &#xff08;3&#xff09;B &#xff08;4&#xff09;D &#xff08;5&#xff09;A &#xff08;6&#xff09;C&#xff08;7&#xff09;D &#xff0…

【学习心得】Python多版本控制

问题描述&#xff1a;本文主要解决Windows系统下的多个Python版本共存问题。 &#xff08;一&#xff09;安装不同版本Python 官方下载链接&#xff1a;Python Releases for Windows | Python.org 下载如图中所示的版本&#xff08;64位Windows系统可执行安装包版本&#xff0…

赞!数字中国建设峰会上的金仓风采

4月30日&#xff0c;第六届数字中国建设成果展览会圆满落幕。人大金仓深度参与本届峰会&#xff0c;在会上发布产品新版本&#xff0c;展出国产数据库前沿的行业解决方案和创新应用成果&#xff0c;出席国资央企SaaS应用服务共享平台伙伴签约仪式&#xff0c;吸引众多用户、伙伴…

面试官:你知道 Spring lazy-init 懒加载的原理吗?

普通的bean的初始化是在容器启动初始化阶段执行的&#xff0c;而被lazy-init修饰的bean 则是在从容器里第一次进行context.getBean(“”)时进行触发。 Spring 启动的时候会把所有bean信息(包括XML和注解)解析转化成Spring能够识别的BeanDefinition并存到Hashmap里供下面的初始…

k210单片机定时器的应用

定时器应该是一个单片机的标准配置&#xff0c;所以k210也是有的&#xff0c;拥有3个定时器&#xff0c;具体的使用方法我们往下看&#xff1a; 分步介绍&#xff1a; 首先是相关模块的使用 构造函数&#xff1a; machine.Timer(id,channel,modeTimer.MODE_ONE_SHOT,period100…

【7. ROS 中的 IMU 惯性测量单元消息包】

欢迎大家阅读2345VOR的博客【6. 激光雷达接入ROS】&#x1f973;&#x1f973;&#x1f973; 2345VOR鹏鹏主页&#xff1a; 已获得CSDN《嵌入式领域优质创作者》称号&#x1f47b;&#x1f47b;&#x1f47b;&#xff0c;座右铭&#xff1a;脚踏实地&#xff0c;仰望星空&#…

vue3回到上一个路由页面

学习链接 Vue Router获取当前页面由哪个路由跳转 在Vue3的setup中如何使用this beforeRouteEnter 在这个路由方法中不能访问到组件实例this&#xff0c;但是可以使用next里面的vm访问到组件实例&#xff0c;并通过vm.$data获取组件实例上的data数据getCurrentInstance 是vue3提…

Java --- springboot2请求参数处理

目录 一、请求参数处理 1.1、请求映射 1.2、自定义请求规则 1.3、请求处理 1.4、普通参数与基本注解 1.4.1、注解 1.5、参数处理原则 1.6、复杂参数 1.7、自定义参数对象 1.8、自定义Converter 一、请求参数处理 1.1、请求映射 // RequestMapping(value "…

c#笔记-下载编辑器

IDE IDE是指集成开发环境&#xff08;Integrated Development Environment&#xff09;&#xff0c;是一种将软件开发所需的软件组合在一起&#xff0c;可以从同一操作界面以统一的操作方式使用的软件包。通常包括代码编辑器、编译器、链接器、调试器、测试工具、版本管理软件等…

自动化运维工具一Ansible Playbook语法实战

目录 一、Ansible Playbook剧本初识 1.1 Ansible Playbook 基本概述 1.1.1 什么是playbook 1.1.2 Ansible playbook 与AD-Hoc的关系 1.2 Ansible Playbook 书写格式 1.2.1安装NFS 服务 1.3 Playbook变量详解 1.3.1 使用 vars定义变量 1.3.2 使用 vars_flies定义变量 …

Java每日一练(20230501)

目录 1. 路径交叉 &#x1f31f;&#x1f31f; 2. 环形链表 &#x1f31f;&#x1f31f; 3. 被围绕的区域 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏…

17自由度人形机器人实现行走功能

1. 功能说明 本文示例将实现R307样机17自由度人形机器人行走的功能。该项目利用探索者平台制作&#xff0c;其驱动系统采用伺服电机。 2. 仿人形机器人结构设计 人型机器人是一种旨在模仿人类外观和行为的机器人&#xff08;robot&#xff09;&#xff0c;尤其特指具有和人类相…

VS快捷键大全 | 掌握这些快捷键,助你调试快人一步

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

Linux常见指令-1

本期我们开始学习Linux&#xff0c;首先我们来学习Linux的常见指令 目录 操作系统是什么 Linux下基本指令 1.ls指令 2.pwd指令 3.cd指令 4.touch指令 5.mkdir指令 6.rmdir指令 && rm 指令 7.man指令 8.cp指令 9.mv指令 10.cat指令 11.more指令 12.less指…