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

目录

  • 引言
  • 一、链表的介绍
  • 二、链表的几种分类
  • 三、不带头单链表的一些常用接口
    • 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. 每个节点还要存储链接下一个节点的指针,这也会造成一点空间浪费;

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

旋极携手西班牙SoC-e公司,为中国客户提供高效可靠TSN通讯解决方案

2023年2月,旋极信息与西班牙SoC-e公司正式签订战略合作协议,成为其在中国区重要合作伙伴。 SoC-e是一家世界领先的基于FPGA技术的以太网通讯解决方案供应商,是一系列IP核开发领域的先锋,为关键任务实施网络化、同步性和安全性提供…

网络参考模型与标准协议(二)-TCP/IP对等模型详细介绍

应用层 应用层为应用软件提供接口,使应用程序能够使用网络服务。应用层协议会指定使用相应的传输层协议,以及传输层所使用的端口等。TCP/IP每一层都让数据得以通过网络进行传输,这些层之间使用PDU ( Paket Data Unit,协议数据单元)彼此交换信…

Virtual安装centos后,xshell连接centos 测试及遇到的坑

首先来一张官方的图--各种网络模式对应的连接状况: 1. 网络使用Host-Only模式动态分配IP,点确定后,centos 上运行 system restart network ,使用ifconfig查看新的ip,XShell可以直接连上centos, 但是由于使用…

【Python】给定n个十六进制正整数,输出它们对应的八进制数。

3.问题描述 给定n个十六进制正整数&#xff0c;输出它们对应的八进制数。 样例输入 2 39 123ABC 样例输出 71 4435274 n int(input()) li [] # 创建列表 for i in range(n):li.append(input()) # 输入数据 for num in li:if len(num) < 100000: # 判断长度是否符…

vue el-table字段点击出现el-input输入框,失焦保存

一、效果展示 当没有数据初始化展示如下&#xff1a; 有数据展示数据&#xff0c;点击出现输入框&#xff0c; 失焦保存修改 二、代码实现 <!-- cell-click"cellClick" 当前单击的单元格 --> <el-tableref"table"size"mini"height&qu…

vue3+vite+SQL.js 读取db3文件数据

前言&#xff1a;好久没写博客了&#xff0c;最近一直在忙&#xff0c;没时间梳理。最近遇到一个需求是读取本地SQLite文件&#xff0c;还是花费了点时间才实现&#xff0c;没怎么看到vite方面写这个的文章&#xff0c;现在分享出来完整流程。 1.pnpm下载SQL.js(什么都可以下)…

值得学习的演示文稿制作范例

1,在第一张幻灯片前插入1张新幻灯片,设置幻灯片大小为“全屏显示(16:9) ”;为整个演示文稿应用“离子会议室”主题,放映方式为“观众自行浏览”;除了标1题幻灯片外其它每张幻灯片中的页脚插入“晶泰来水晶吊坠”七个字。 2,第一张幻灯片的版式设置为“标题幻灯片”,主标题为“…

逻辑漏洞(越权)

逻辑漏洞&#xff08;越权&#xff09; 0x01 何为逻辑漏洞 逻辑漏洞是指&#xff0c;在编写程序的时&#xff0c;一个流程处理处理逻辑&#xff0c;不够谨慎或逻辑不完整&#xff0c;从而造成验证失效、敏感信息暴露等问题&#xff0c;这类问题很难利用工具去发现&#xff0c…

高防CDN有什么作用?

分布式拒绝服务攻击&#xff08;DDoS攻击&#xff09;是一种针对目标系统的恶意网络攻击行为&#xff0c;DDoS攻击经常会导致被攻击者的业务无法正常访问&#xff0c;也就是所谓的拒绝服务。 常见的DDoS攻击包括以下几类&#xff1a; 网络层攻击&#xff1a;比较典型的攻击类…

vue3父组件提交校验多个子组件

实现功能&#xff1a;在父组件提交事件中校验多个子组件中的form 父组件&#xff1a; <script setup lang"ts">import {ref, reactive} from vueimport childForm from ./childForm.vueimport childForm2 from ./childForm2.vuelet approvalRef ref()let ap…

Arcgis小技巧【16】:ArcMap的那些功能在ArcGIS Pro里都去哪儿了?

有部分小伙伴现在已经用上了ArcGIS Pro&#xff0c;但可能还会有些不习惯。 一个很重要的原因&#xff0c;原来在ArcMap中的一些功能&#xff0c;好像在Pro里消失了。 不排除一些功能确实被移除了&#xff0c;但大部分其实是因为UI的变化&#xff0c;给放在了别的地方。 这里…

Flink 运行架构和核心概念

Flink 运行架构和核心概念 几个角色的作用&#xff1a; 客户端&#xff1a;提交作业JobManager进程 任务管理调度 JobMaster线程 一个job对应一个JobMaster 负责处理单个作业ResourceManager 资源的分配和管理&#xff0c;资源就是任务槽分发器 提交应用&#xff0c;为每一个…

矩阵理论——Gerschgorin定理,以及用python绘制Gerschgorin圆盘动图

矩阵理论——Gerschgorin定理&#xff0c;以及用python绘制Gerschgorin圆盘动图 在矩阵的特征值估计理论当中&#xff0c;有一节是盖尔圆盘定理&#xff1a; 对于一个n阶复数矩阵A&#xff0c;每个特征值lambda位于至少一个Gerschgorin圆盘中&#xff0c;这些圆盘的中心为矩阵…

【18年扬大真题】定义一个Point类,要求如下所述。(1)用构造函数初始化Point类的对象(2)定义函数Distance,计算平面上两点之间的距离

【18年扬大真题】定义一个Point类&#xff0c;要求如下所述。 &#xff08;1&#xff09;用构造函数初始化Point类的对象 &#xff08;2&#xff09;定义函数Distance&#xff0c;计算平面上两点之间的距离 #include<stdio.h> #include<math.h> typedef struct {d…

【Python】12 GPflow安装

概述 GPflow 是一个基于TensorFlow 在 Python 中构建高斯过程模型的包。高斯过程是一种监督学习模型。 高斯过程的一些优点是&#xff1a; 不确定性是高斯过程的固有部分。高斯过程可以在不知道答案时告诉您。适用于小型数据集。如果您的数据有限&#xff0c;高斯过程可以从…

Attingo:西部数据部分SSD存在硬件设计制造缺陷

今年5月&#xff0c;西部数据SanDisk Extreme Pro硬盘陆续有用户反馈有故障发生&#xff0c;用户反馈最多的问题是数据丢失和硬件损坏。8月份&#xff0c;因为这个事情&#xff0c;还被爆出&#xff0c;西部数据面临用户的集体诉讼。 近期&#xff0c;有一个专门从事数据恢复的…

记录下学的性能优化

一、性能优化的指标和工具 1.1 谷歌浏览器 拿淘宝网站为例,可以看到当前网页的加载信息 这个是瀑布图,瀑布图有横向和纵向 横向是具体的加载数据,悬浮看详情列表,可以看出下载时最后一个步骤,在这之前会先排队,浏览器会对优先级进行安排,它会对高优先级的请求优先请求.然后通…

视频剪辑方法:一键批量调整色调的高效技巧

在视频剪辑的过程中&#xff0c;色调调整是一项非常重要的工作。它能够改变影片的氛围、情感和视觉效果&#xff0c;更好地沉浸在影片的情境中。然而&#xff0c;对于许多视频剪辑师来说&#xff0c;批量调整色调是一项非常繁琐的任务&#xff0c;需要耗费大量的时间和精力。色…

【考研】数据结构(更新到顺序表)

线性表的定义和基本操作 学习目标 线性表定义&#xff1a;具有相同数据类型的n个数据元素的有序序列。 顺序表定义&#xff1a; 特点 基本操作 定义 静态&#xff1a; #include<stdio.h> #include<stdlib.h>#define MaxSize 10//静态 typedef struct{int …