【数据结构】详解链表结构

news2024/11/14 1:17:48

目录

  • 引言
  • 一、链表的介绍
  • 二、链表的几种分类
  • 三、不带头单链表的一些常用接口
    • 3.1 动态申请一个节点
    • 3.2 尾插数据
    • 3.3 头插数据
    • 3.4 尾删数据
    • 3.5 头删数据
    • 3.6 查找数据
    • 3.7 pos位置后插入数据
    • 3.8 删除pos位置数据
    • 3.9 释放空间
  • 四、带头双向链表的常见接口
    • 4.1创建头节点(初始化)
    • 4.2pos位置前插入
    • 4.3删除pos位置数据
    • 4.4其他
  • 五、总结

引言

上篇博客已经介绍了顺序表的实现:【数据结构】详解顺序表。最后在里面也谈及了顺序表结构的缺陷,即效率低,空间浪费等等问题,那么为了解决这些问题,于是乎我们引入了链表的概念,下面将对链表结构进行讲解

一、链表的介绍

首先肯定会问,到底什么是链表?链表的概念链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
在结构上其与火车的结构相似,分为一个个节点,再将每个节点连接起来,就形成了一个链表,其大致结构如下:
在这里插入图片描述
但还要几点需要注意

  1. 链式结构在逻辑上是连续的,但在物理空间上不一定是连续的
  2. 这些节点一般是在堆上申请出来的,即使用malloc函数来动态申请空间;
  3. 每当需要增加一个数据时,便可申请一段空间,空间可能连续也可能不连续。

二、链表的几种分类

链表的结构大致可以分为8类,即:带头/不带头单向链表,带头/不带头双向链表,带头/不带头单向循环链表,带头/不带头双向循环链表。 今天我所介绍的是其中最简单的结构和最复杂的结构:

  1. 单向不带头不循环链表:
    在这里插入图片描述
    单向不带头不循环链表结构简单,但实现起来并不简单且复杂度高,所以一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 双向带头循环链表:
    在这里插入图片描述
    带头双向循环链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。表面上看这结构十分的复杂,但在后面实现函数时会发现这种结构会带来很多优势,实现起来反而更简单了,复杂度也大大降低

三、不带头单链表的一些常用接口

定义如下结构体,表示链表的一个节点:

typedef int SLDataType;

typedef struct SlistNode
{
    SLDataType val;//所需保存的数据
    struct SListNode* next;//结构体指针,指向下一个节点的地址
}SLNode;

3.1 动态申请一个节点

为了使链表在各个函数中都可以使用,所以我们需要动态开辟内存来创建节点,再通过指针将他们相连接。在CreatNode()函数中我们创建节点并将他们初始化:

//动态申请节点
SLNode* CreatNode(SLTDateType x)
{
    SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
    //检测
    if(newnode == NULL)
    {
        perror("CreatNode()::malloc");
        return;
    }
    newnode->val = x;
    newnode->next = NULL;
    return newnode;
}

3.2 尾插数据

根据一般逻辑,我们想要尾插那就要先创建新节点并找到尾节点。那么我们定义指针tail,然后利用循环找尾节点再链接新节点tail->next = newnode,另外还要额外判断链表为空的情况,此情况直接尾插即可,具体如下:

//尾插
void SLPushBack(SLNode** pplist, SLDateType x)
{
	SLNode* newnode = CreatNode(x);
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
	    //找尾
		SLNode* tail = *pplist;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
        //链接
		tail->next = newnode;
	}
}

3.3 头插数据

头插就比较简单了,只需要注意一点:不额外定义变量时,要先将新节点链到链表,即newnode->next = *pplist然后再改头节点,即*pplist = newnode,如下:

void SLPushFront(SLNode** pplist, SLDateType x)
{
	assert(pplist);
	SLNode* newnode = CreatNode(x);
	newnode->next = *pplist;
	*pplist = newnode;
}

3.4 尾删数据

同样想要尾删,那就必须先找到尾节点然后释放空间。但释放完空间后,上一个节点的next仍然指向释放空间的地址,这就可能造成越界访问,野指针问题。所以我们还需要记录尾节点的上一个节点tailPrev,然后通过这个指针将此节点next置为NULL。此外还需用assert()检测链表不为NULL分类讨论链表只有一个节点和有多个节点的情况。如下:

//尾删
void SLPopBack(SLNode** pplist)
{
    assert(pplist && *pplist);
	SLNode* tailPrev = NULL;
	SLNode* tail = *pplist;
	// 1.只有一个节点
	if (tail->next == NULL)
	{
		free(tail);
		*pplist = NULL;
	}
	// 2.两个及以上的节点
	else
	{
	    //找尾及上一个节点
		while (tail->next)
		{
			tailPrev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;
		tailPrev->next = NULL;
	}
}

3.5 头删数据

头删数据时,链表同样不能为空,另外头删无需判断链表节点数问题,这就比较容易实现了:

void SLPopFront(SLNode** pplist)
{
    //不为空
    assert(pplist && *pplist);
    //记录第一个节点
    SLNode* first= *pplist;
    *pplist = (*pplist)->next;
    free(first);
}

3.6 查找数据

给定一个val,再链表中向后寻找,找到时返回此节点地址pos,未找到返回NULL。我们只需定义一个结构体指针SLNode* cur = plist;,让他向后走,找到val时返回cur,直到cur = NULL时循环结束并返回NULL。因为这里无需改变链表指向,所以可以直接传一级指针。

SLNode* SLFind(SLNode* plist, SLDateType x)
{
	SLNode* cur = plist;
	while (cur)
	{
	    //寻找val
		if (cur->val == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

3.7 pos位置后插入数据

如下图,先创建一个节点newnode,然后将newnode->next指向pos位置的下一个节点,最后将pos->next指向新节点。
在这里插入图片描述
当然pos != NULL

//指定位置插入
void SLInsertAfter(SLNode* pos, SLDataType x)
{
    assert(pos);
    SLNode* newnode = CreatNode(x);
    newnode->next = pos->next;
    pos->next = newnode;
}

3.8 删除pos位置数据

先通过循环找到pos前一个节点地址posPrev,和后一个节点地址posNext,然后释放pos节点,链接posPrevposNext
在这里插入图片描述
同样pos != NULL,还有一点是当pos为头节点,就相当于头删,但无需判断,同样适用。

void SLErase(SLNode* pos, SLNode** pplist)
{
    assert(pos && pplist);
    SLNode* posPrev = *pplist, *posNext = pos->next, *cur = *pplist;
    //找pos前一个节点
    while(cur != pos)
    {
        posPrev = cur;
        cur = cur->next;
    }
    //链接
    posPrev->next = posNext;
    free(pos);
}

3.9 释放空间

因为这是一个链式结构,且每个节点是malloc动态开辟的,所以最后要将所以节点释放,否则会造成内存泄漏问题。只需定义一个结构体指针SLNode* cur = *pplist,让他向后依次释放节点,直到cur = NULL

void SLDestroy(SLNode** pplist)
{
    assert(pplist);
    SLNode* cur = *pplist;
    while(cur)
    {
        //记录下一个节点
        SLNode* next = cur->next;
        free(cur);
        cur = next;
    }
}

四、带头双向链表的常见接口

通过上面对单向不循环链表的介绍,我们不难发现其实单链表的尾插,尾删和指定位置删除其实效率是不高的,时间复杂度为O(n)。而双向带头循环链表是不存在这个问题的,且因为链表带头节点的原因,在函数传参是无需用到二级指针,在实现函数时也会发现很多时候也不需要单独判断链表没节点的情况,因为头节点本身就是一个节点,这也大大降低了代码的难度
双向带头循环链表的每个节点都包含两个指针:一个指向上一个节点,一个指向下一个节点。那么便可这样设计节点:

typedef int DataType;
//节点设计
typedef struct DListNode
{
	DataType val;
	struct DListNode* _prev;//指向上一个节点的指针
	struct DListNode* _next;//指向下一个节点的指针
}DLNode;

4.1创建头节点(初始化)

头节点和其他节点的结构是相同的,就相当于链表自带一个节点,并将此节点初始化成以下格式:
在这里插入图片描述

DLNode* InitDLNode(DLNode* phead)
{
    //创建
    DLNode* head = (DLNode*)malloc(sizeof(DLNode));
	if (head == NULL)
	{
		perror("InitDLNode()::malloc");
		return;
	}
	//形成循环结构
	head->_prev = head;
	head->_next = head;
	head->val = -1;
	return head;
}

4.2pos位置前插入

对于pos位置之前插入,可以先通过pos->_prev找到前一个节点的地址,然后再进行插入操作。因为是双向循环链表的原因,找pos前一个节点也不需要循环,时间复杂度只有O(1)
在这里插入图片描述
事实上当链表无有效节点(即只有头节点)也不需要单独判断,这样降低了代码难度,具体实现代码如下:

//指定位置插入
void DLNodeInsert(DLNode* pos, DataType x)
{
	assert(pos);
	DLNode* posPrev = pos->_prev;//pos前一个节点
	DLNode* newnode = CreatNode(x);//创建新节点
	//链接
	posPrev->_next = newnode;
	newnode->_prev = posPrev;
	pos->_prev = newnode;
	newnode->_next = pos;
}

4.3删除pos位置数据

删除pos位置的数据,我们可以通过pos->_prev找到上一个节点的地址,再通过pos->_next找到下一个节点的地址。然后将这两个节点链接起来,并释放pos节点,如下:
在这里插入图片描述
当只有一个头节点时我们还需额外判断一下,代码如下:

void DLNodeErase(DLNode* pos)
{
	assert(pos);
	assert(pos->_next != pos);//不能为头节点
	DLNode* posPrev = pos->_prev, * posNext = pos->_next;
	//链接
	posPrev->_next = posNext;
	posNext->_prev = posPrev;
	free(pos);
}

4.4其他

还有头删/插,尾删/插这四个函数,但这四个函数并不需要额外实现,因为:
头插/删可以当作pos = phead->_next的指定位置插入/删除,尾删也可以当作pos = phead->_prev的指定位置删除,尾插则是pos = phead的位置。头/尾删这两个pos分别代表头节点的后一个和前一个,且判断assert(phead != phead->_next)也是必要的,这里就不代码实现了。


五、总结

对比于顺序表我们发现链表有很多优点,也有一些缺点:
优点:

  1. 链表的空间浪费较小,按需开辟空间;
  2. 任意位置插入和删除数据效率更高,实现了O(1)时间复杂度;

缺点:

  1. 不支持随机访问数据(致命缺陷);
  2. 每个节点还要存储链接下一个节点的指针,这也会造成一点空间浪费;

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

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

相关文章

everything的高效使用方法

目录 前言1 everything的简单介绍2 常用搜索3 语法搜索4 正则表达式搜索5 服务器功能 前言 本文介绍everything软件的高效使用方法,everything是一款在系统中快速搜索文件的软件,能够帮助人们快速定位需要查找的文件。首先介绍everything软件的作用和使…

摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕,各大品牌纷纷晒出优异的成绩单,摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称,在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁,多个平台数据都表现出极度异常…

【开源】基于Vue.js的高校宿舍调配管理系统

项目编号: S 051 ,文末获取源码。 \color{red}{项目编号:S051,文末获取源码。} 项目编号:S051,文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能需求2.1 学生端2.2 宿管2.3 老师端 三、系统…

【Python进阶】近200页md文档14大体系知识点,第4篇:linux命令和vim使用

本文从14大模块展示了python高级用的应用。分别有Linux命令,多任务编程、网络编程、Http协议和静态Web编程、htmlcss、JavaScript、jQuery、MySql数据库的各种用法、python的闭包和装饰器、mini-web框架、正则表达式等相关文章的详细讲述。 全套Python进阶笔记地址…

OFDM通信系统仿真之交织技术

文章目录 前言一、交织1、概念2、图形举例3、交织的位置 二、MATLAB仿真1、MATLAB 程序2、仿真结果 前言 之前的博客:OFDM深入学习及MATLAB仿真 中有对交织的概念进行讲解,但讲解还是比较浅显,且仿真实现时并没有加入交织及解交织流程&#…

【电路笔记】-欧姆定律

欧姆定律 文章目录 欧姆定律1、概述2、AC电路的等效性2.1 输入电阻2.2 输入电感2.3 输入电容 3、欧姆定律的局部形式3.1 介绍和定义3.2 德鲁德模型(Drude Model)3.3 局部形式表达式 4、电阻和宏观欧姆定律5、总结 电流、电压和电阻之间的基本关系被称为欧姆定律,可能…

解决龙芯loongarch64服务器编译安装Python后yum命令无法使用的问题“no module named ‘dnf‘”

引言 在使用Linux系统时,我们经常会使用yum来管理软件包。然而,有时候我们可能会遇到yum不可用的情况,其中一个原因就是Python的问题。本文将介绍Python对yum可用性的影响,并提供解决方案。 问题引发 正常情况下,安装linux系统后,yum命令是可用状态,升级Python版本后,…

CPU版本的pytorch安装

1.安装:Anaconda3 2.安装:torch-2.0.1cpu-cp311 2.安装:torchvision-0.15.2cpu-cp311-cp311-win_amd64 测试是否安装成功 cmd 进入python import torch print(torch.__version__) print(torch.cuda.is_available())

使用Docker/K8S/Helm部署项目流程

假设项目已经开发完成,部署流程如下: 一、制作镜像: 1、创建nginx配置文件default.conf server {listen 80;server_name localhost; # 修改为docker服务宿主机的iplocation / {root /usr/share/nginx/html;index index.html ind…

服务器端请求伪造(SSRF)

概念 SSRF(Server-Side Request Forgery,服务器端请求伪造) 是一种由攻击者构造形成的由服务端发起请求的一个安全漏洞。一般情况下,SSRF是要攻击目标网站的内部系统。(因为内部系统无法从外网访问,所以要把目标网站当做中间人来…

盼望许久的百度熊终于收到了

文|洪生鹏 我怀着激动的心情,终于收到了百度熊礼品。 在我想象中,这只熊应该很大,能够填满我的怀抱。 但当我打开礼盒的那一刻,我有些惊讶。 它居然这么小,与我预期的相差甚远。 不过,当我们仔细一看&#…

大厂数仓专家实战分享:企业级埋点管理与应用

一.什么是埋点 埋点(Event Tracking),是互联网数据采集工作中的一个俗称,正式应该叫事件跟踪,英文为 Event Tracking,它主要是针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。 二.埋…

中国互联网格局改变的重点,在于真正走向海外,打破美国垄断

媒体报道指字节跳动上半年的营收达到540亿美元,超过了其他互联网企业,这是国内互联网行业格局发生重大变化的证明,那么是什么原因导致了这一格局的改变呢? 中国互联网的发展也有20多年了,这20多年涌现了一大批互联网企…

文件夹改名:批量随机重命名文件夹,让整理更轻松

在日常生活和工作中,文件夹重命名是一件非常常见的事情。有时候,可能需要批量处理文件夹,为其加上统一的名称,或者按照某种特定的规则来重命名。然而,当我们手动进行这些操作时,会消耗大量的时间和精力。这…

Redis:Java客户端

前言 "在当今大数据和高并发的应用场景下,对于数据缓存和高效访问的需求日益增长。而Redis作为一款高性能的内存数据库,以其快速的读写能力和丰富的数据结构成为众多应用的首选。与此同时,Java作为广泛应用于企业级开发的编程语言&…

三极管-开关电路-电路电子-嵌入式开发-物联网开发-电子元件

一、概述 本文我们主要讲解电子电路中十分重要的元件--三极管。三极管常常被用来当作开关或作为放大电流的作用,下面我们将主要围绕着其作为开关电路的使用来介绍三极管。 二、分类 学习三极管前,我们必须认识三极管的三级,包含箭头的一端为发…

【论文阅读笔记】Deep learning for time series classification: a review

【论文阅读笔记】Deep learning for time series classification: a review 摘要 在这篇文章中,作者通过对TSC的最新DNN架构进行实证研究,探讨了深度学习算法在TSC中的当前最新性能。文章提供了对DNNs在TSC的统一分类体系下在各种时间序列领域中的最成功…

(1)(1.17) Maxbotix 模拟声纳

文章目录 前言 1 连接到Pixhawk 2 通过Mission Planner进行设置 3 测试传感器 4 参数说明 前言 XL-Maxbotix-EZ 系列模拟声纳(XL-MaxSonar-EZ0、EZ4 和 EZL0)是相对便宜的短距离(7m 至 10m)测距仪,主要设计用于室…

Linux系统中Qt应用程序确保使用集成显卡进行图形渲染

背景:有一个Qt开发的应用程序,使用了Qt的OpenGL模块来渲染QImage加载的图片,当应用程序切换到图片渲染界面时,系统CPU占用率立马到了100%多,容易造成程序卡顿,可设备是自带集成显卡的,期望效果是…

7 Redis的PipeLine

PipeLine的作用是批量执行命令 redis的性能瓶颈基本上是网络 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.…