线性表之链表

1、链表概述

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

顺序表的存储位置可以用一个简单直观的公式表示,它可以随机存取表中任意一个元素,但插入和删除需要移动大量元素。链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻,它通过“链”建立起元素之间的逻辑关系,因此插入和删除操作不需要移动元素,只需要修改指针,这也意味着链表失去了可随机存取的特点。

2、链表的分类

链表结构种类多样,可以按照是否带头、是否循环、单向或者双向大致分类。

(1)带头结点和不带头结点

 (2)单向链表和双向链表

 (3)循环链表和非循环链表

以上情况组合就有8中链表结构,但实际中应用最多的链表结构是无头单向非循环链表和带头双向循环链表。下面介绍两种链表的基本实现。

3、无头单向非循环链表的基本实现

为了建立数据元素之间的线性关系,链表结点除了存放数据,还需要存放一个指向其后继的指针。单链表可以解决顺序表需要大量连续存储单元的缺点。由于单链表的元素离散地分布在存储空间中,所以单链表时非随机存取的存储结构。

单链表的结点类型描述如下。

typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;

3.1单链表的打印

为了观感上更贴近单链表的定义,打印时先打印结点的值,“->”表示链表的指针,打印完所有元素后再打印“NULL”。

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

3.2单链表的销毁

销毁时传入的是一级指针

void SLTDestroy(SLTNode* phead)
{SLTNode* cur = phead;while (phead){cur = phead;phead = phead->next;free(cur);cur = NULL;}printf("success\n");
}

3.3头插法插入结点

使用头插法插入新结点时,不用考虑链表是否为空,因为不涉及空指针的引用,直接插入即可。

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuySLTNode(x);newnode->next = *pphead;*pphead = newnode;
}

3.4尾插法插入结点

使用尾插法插入新结点时需要判断链表是否为空,因为插入过程中涉及到了空指针的引用。

当链表为空时,新结点即链表第一个结点;当链表不为空时,先找到链表的尾结点,然后直接将新结点插到尾结点后面。

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = BuySLTNode(x);if (*pphead == NULL)*pphead = newnode;else{SLTNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}

3.5头删法删除结点

删除之前需要判断链表是否为空,链表不为空时,删除第一个结点。

void SLTPopFront(SLTNode** pphead)
{assert(pphead);assert(*pphead);SLTNode* del = *pphead;*pphead = (*pphead)->next;free(del);del = NULL;
}

3.6尾删法删除结点

删除之前同样需要判断链表是否为空,此外还需要判断链表是否只有一个结点,因为删除过程中涉及到空指针的引用。如果链表只有一个结点,直接将该结点删除释放;如果链表有多个元素,找到倒数第二个结点,然后删除该结点的后继。

void SLTPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead);if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* tail = *pphead;while (tail->next->next != NULL){tail = tail->next;}free(tail->next);tail->next = NULL;}
}

3.7按值查找

从头开始依次对比,如果找到值为x的结点则返回该结点的地址,如果没找到则返回空指针。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{assert(phead);SLTNode* cur = phead;while (cur){if (cur->data == x)return cur;cur = cur->next;}return NULL;
}

3.8在指定位置之前插入结点

插入分为链表只有一个结点和有多个结点这两种情况。当链表只有一个元素或者指定位置为第一个结点时,相当于头插法;当链表有多个元素或者指定位置为其他结点时,找到指定位置的前驱,然后修改其前驱的后继以及新结点的后继。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);if (pos == *pphead)SLTPushFront(pphead, x);else{SLTNode* posPrev = *pphead;SLTNode* newnode = BuySLTNode(x);while (posPrev->next != pos){posPrev = posPrev->next;}posPrev->next = newnode;newnode->next = pos;}
}

3.9在指定位置之后插入结点

在指定位置之后插入不用考虑链表有几个结点,直接插入即可。注意,要先修改新结点的后继,再修改指定位置结点的后继。

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySLTNode(x);newnode->next = pos->next;pos->next = newnode;
}

3.10删除指定位置的结点

当链表只有一个元素或者指定位置为第一个结点时,相当于链表的头删;当链表有多个元素或者指定位置为其他结点时,找到指定位置结点的前驱,并修改其后继,再删除并释放指定位置结点。

void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);if (pos == *pphead)SLTPopFront(pphead);else{SLTNode* posPrev = *pphead;while (posPrev->next != pos){posPrev = posPrev->next;}posPrev->next = pos->next;free(pos);pos = NULL;}
}

3.11删除指定位置之后的结点

在指定位置之后删除不用考虑链表有几个结点,直接删除即可。

void SLTEraseAfter(SLTNode* pos)
{assert(pos);SLTNode* del = pos->next;pos->next = pos->next->next;free(del);del = NULL;
}

4、带头双向循环链表的基本实现

单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问某个结点的前驱结点时,只能从头开始遍历,访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(n)。为了克服单链表的这些缺点,引入双链表,双链表中有两个指针prev和next,分别指向前驱结点和后继结点。

双链表中结点类型的描述如下:

typedef int LTDataType;
typedef struct ListNode
{LTDataType data;struct ListNode* prev;struct ListNode* next;
}LNode;

双链表可以很方便找到前驱结点,因此,插入、删除操作的时间复杂度为O(1)。

此外,链表带头结点,无论链表是否为空,其头指针都是指向头结点的非空指针,因此在链表的插入和删除中空链表和非空链表的处理得到了统一。

4.1初始化双向链表

为了减少二级指针的使用,在初始化双链表时,使用一级指针定义并返回头结点。因此初始化时直接建立一个头结点并返回头结点的地址。

因为后续实现链表插入时还需要建立新结点,为了简化代码以及增强代码的可读性,定义一个建立新结点的函数,后续需要建立新结点时直接调用该函数即可。建立新结点函数如下。

LTNode* BuyLTNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");return NULL;}newnode->next = NULL;newnode->prev = NULL;newnode->data = x;return newnode;
}

初始化双链表函数如下。

LTNode* Init()
{LTNode* head = BuyLTNode(-1);head->next = head;head->prev = head;return head;
}

4.2双向链表的打印

需要注意的是,打印时从phead的next开始打印。为了观感上更贴近双链表的定义,打印时先打印“guard”表示头结点,“<==>”表示链表的双指针,打印完所有元素后再打印一次“guard”表示尾结点的链接到头结点。

void LTPrint(LTNode* phead)
{LTNode* cur = phead->next;printf("guard<==>");while (cur != phead){printf("%d<==>", cur->data);cur = cur->next;}printf("guard\n");
}

4.3双向链表的销毁

双链表的销毁同单链表的销毁类似,这里使用的仍是一级指针,所以需要用户在调用完销毁函数后,手动置空头结点。

void LTDestory(LTNode* phead)
{LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}
}

4.4头插法插入结点

在使用头插法插入新结点时,要注意指针修改顺序,指针顺序虽然不是唯一的,但也不是任意的。新结点前驱和后继的修改必须在头结点后继的修改之前进行,否则头结点的后继结点的指针就会丢掉,导致插入失败。如图所示,1和2必须在4之前进行。

void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyLTNode(x);newnode->next = phead->next;phead->next->prev = newnode;newnode->prev = phead;phead->next = newnode;
}

还有一种方法,可以不用考虑指针修改的先后顺序,就是重新定义一个结点存放头结点的后继结点,这样就不用担心头结点的后继指针丢失的问题了。

void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyLTNode(x);LTNode* first = phead->next;  newnode->prev = phead;  phead->next = newnode;newnode->next = first;first->prev = newnode;
}

4.5尾插法插入结点

使用尾插法时,不用像单链表那样遍历找尾,头结点的前驱结点就是尾结点。插入时同样需要注意指针的修改顺序,1和2必须要在3之前进行。

和头插法类似,重新定义一个结点保存头结点的前驱结点,就不用考虑指针修改的顺序了。

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyLTNode(x);LTNode* tail = phead->prev;tail->next = newnode;newnode->prev = tail;phead->prev = newnode;newnode->next = phead;
}

4.6头删法删除结点

在删除之前需要将头结点的后继结点保存起来,否则无论在修改指针之前free后继结点还是在修改指针之后free后继结点都会出现错误。

 注意,当链表为空时(只有头结点)时不能继续删除,所以在删除之前需要判断链表是否为空。

bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}
void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTNode* first = phead->next;phead->next = first->next;first->next->prev = phead;free(first);
}

4.7尾删法删除结点

在删除之前需要将尾结点保存起来,为了方便,将尾结点的前驱结点也保存一下。与头删法同样,在删除前需要判断双链表是否为空,如果为空则不能继续删除。

void LTPopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTNode* tail = phead->prev;LTNode* tailPrev = tail->prev;tailPrev->next = phead;phead->prev = tailPrev;free(tail);
}

4.8按值查找

需要注意的是,从头结点的后继结点开始查找,并且停止查找的条件是当前结点不等于头结点。

LTNode* LTFind(LTNode* phead, LTDataType x)
{LTNode* cur = phead->next;while (cur != phead){if (cur->data == x)return cur;cur = cur->next;}return NULL;
}

4.9在指定位置之前插入结点

在插入之前先保存pos的前驱结点,这样在插入时就不用考虑指针的修改顺序了。

void LTInsert(LTNode* pos, LTDataType x)
{LTNode* newnode = BuyLTNode(x);LTNode* posPrev = pos->prev;posPrev->next = newnode;newnode->prev = posPrev;newnode->next = pos;pos->prev = newnode;
}

 头插法和尾插法插入结点可以通过该函数的复用实现。

//头插法
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTInsert(phead->next, x);
}//尾插法
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTInsert(phead, x);
}

注意,头插法传入的pos为头结点的后继结点;尾插法传入的pos为头结点,因为头结点的前驱结点就是尾结点。

4.10删除指定位置的结点

在删除之前先保存pos的前驱结点和后继结点。

void LTErase(LTNode* pos)
{LTNode* posPrev = pos->prev;LTNode* posNext = pos->next;free(pos);posPrev->next = posNext;posNext->prev = posPrev;
}

同样地,头删法和尾删法删除结点可以通过该函数的复用实现。

//头删法
void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTErase(phead->next);
}//尾删法
void LTPopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTErase(phead->prev);
}

注意,头删法传入的pos是头结点的后继结点;尾删法传入的pos是头结点的前驱结点。

5、顺序表与链表的比较

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

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

相关文章

深入理解 PostgreSQL 的架构和内部工作原理

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

Postman怎么做接口测试-以简单的登录接口为例

我们就以登录某测试系统为例子&#xff0c;实现在Postman上做接口测试 一、首先打开系统首页首页&#xff0c;做一个登录操作&#xff08;目的是获取接口url及参数&#xff09;&#xff1a;一般在公司做接口测试的时候页面还没有出来&#xff0c;我们需要根据接口文档进行接口…

kafka第三课-可视化工具、生产环境问题总结以及性能优化

一、可视化工具 https://pan.baidu.com/s/1qYifoa4 密码&#xff1a;el4o 下载解压之后&#xff0c;编辑该文件&#xff0c;修改zookeeper地址&#xff0c;也就是kafka注册的zookeeper的地址&#xff0c;如果是zookeeper集群&#xff0c;以逗号分开 vi conf/application.conf 启…

Python 逻辑回归:理论与实践

文章目录 1. 介绍1.1 什么是逻辑回归&#xff1f;1.2 逻辑回归的应用领域 2. 逻辑回归的原理2.1 Sigmoid 函数2.2 决策边界2.3 损失函数 3. 逻辑回归的实现3.1 数据准备3.2 创建逻辑回归模型3.3 模型训练3.4 模型预测3.5 模型评估 4. 可视化决策边界4.1 绘制散点图4.2 绘制决策…

基于SaaS模式的Java基层卫生健康云HIS系统源码【运维管理+运营管理+综合监管】

云HIS综合管理平台 一、模板管理 模板分为两种&#xff1a;病历模板和报表模板。模板管理是运营管理的核心组成部分&#xff0c;是基层卫生健康云中各医疗机构定制电子病历和报表的地方&#xff0c;各医疗机构可根据自身特点特色定制电子病历和报表&#xff0c;制作的电子病历…

php 手机加*星 【字符串】

场景&#xff1a;展示手机号时&#xff0c;避免暴露隐私信息&#xff0c;因此需要给手机号加*号 代码 /*** 手机号码隐私加星* param string $mobile 手机号*/ function mobileToStar(string $mobile) { // 正则检测手机号if(!preg_match(/^1[3456789]\d{9}$/,$mobile)){// …

Docker 容器生命周期:创建、启动、暂停与停止----从创建到停止多角度分析

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

Sysbench测试工具详解

文章目录 Sysbench安装准备测试数据使用yum安装的路径进行测试测试结果 Sysbench Sysbench 是一个常用的多线程性能测试工具&#xff0c;可用于评估数据库系统和硬件的性能。它支持多种基准测试&#xff0c;包括 OLTP (Online Transaction Processing)、CPU、文件 I/O、内存等。…

代码随想录| 图论04 查并集 ●查并集理论知识 ●1971.寻找图中是否存在路径 ●684.冗余连接 ●685.冗余连接II

#查并集理论知识 并查集用处&#xff1a;解决连通性问题 将两个元素添加到一个集合中。判断两个元素在不在同一个集合 思路&#xff1a;将三个元素A&#xff0c;B&#xff0c;C &#xff08;分别是数字&#xff09;放在同一个集合&#xff0c;其实就是将三个元素连通在一起&a…

Python 算法基础篇:插入排序和希尔排序

Python 算法基础篇&#xff1a;插入排序和希尔排序 引言 1. 插入排序算法概述2. 插入排序算法实现实例1&#xff1a;插入排序 3. 希尔排序算法概述4. 希尔排序算法实现实例2&#xff1a;希尔排序 5. 插入排序与希尔排序的对比总结 引言 插入排序和希尔排序是两种常用的排序算法…

017-从零搭建微服务-系统服务(四)

写在最前 如果这个项目让你有所收获&#xff0c;记得 Star 关注哦&#xff0c;这对我是非常不错的鼓励与支持。 源码地址&#xff08;后端&#xff09;&#xff1a;https://gitee.com/csps/mingyue 源码地址&#xff08;前端&#xff09;&#xff1a;https://gitee.com/csps…

【实战】 七、Hook,路由,与 URL 状态管理(下) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十三)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表三、TS 应用&#xff1a;JS神助攻 - 强类型四、JWT、用户认证与异步请求五、CSS 其实很简单 - 用 CSS-in-JS 添加样式六、用户体验优化 - 加载中和错误状态处理七、Hook&…

el-upload将上传的图片转为base64

el-upload将上传的图片转为base64 文章目录 el-upload将上传的图片转为base641. el-upload属性设置2. 图片转base64格式方法3. 在绑定的方法中调用 elementui使用el-upload时将选择的图片转为base64格式,然后再上传 1. el-upload属性设置 使用组件&#xff0c;然后on-change绑定…

【NLP】如何使用Hugging-Face-Pipelines?

一、说明 随着最近开发的库&#xff0c;执行深度学习分析变得更加容易。其中一个库是拥抱脸。Hugging Face 是一个平台&#xff0c;可为 NLP 任务&#xff08;如文本分类、情感分析等&#xff09;提供预先训练的语言模型。 本博客将引导您了解如何使用拥抱面部管道执行 NLP 任务…

代码随想录算法训练营day45 70.爬楼梯(进阶) 322.零钱兑换 279.完全平方数

题目链接70.爬楼梯(进阶) class Solution {public int climbStairs(int n) {int[] dp new int[n1];int m 2;dp[0] 1;for(int i 1; i < n; i){for(int j 1; j < m; j){if(i > j)dp[i] dp[i-j];}}return dp[n];} }题目链接322.零钱兑换 class Solution {public …

超详细图文教程:3DS Max 中创建低多边形游戏长剑模型

推荐&#xff1a; NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 在此&#xff0c;由两部分组成的教程的第一部分中&#xff0c;我将向您展示如何&#xff1a; 对剑柄进行建模剑的护手模型剑刃建模 1. 如何制作剑柄 步骤 1 在本教程中使用正交视图。要更改视图&#x…

python安装OpenCV

安装OpenCV pip install opencv-pythonpython OpenCV 打开摄像头 import cv2WIDTH 1080 HEIGHT 720cap cv2.VideoCapture(0, cv2.CAP_DSHOW) cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)while True:ok,frame cap.read()# pri…

阿里巴巴前端开发规范

前言 规范的目的是为了编写高质量的代码&#xff0c;让你的团队成员每天的心情都是愉悦的&#xff0c;大家在一起是快乐的。 现在软件架构的复杂性需要协同开发完成&#xff0c;如何高效地协同呢&#xff1f;无规矩不成方圆&#xff0c;无规范难以协同&#xff0c;比如&#xf…

机器学习概念

文章目录 一、机器学习概念1. 机器学习基本概念2. 基于规则的学习3. 基于模型学习4. 机器学习数据集描述二、机器学习分类1. 监督学习1.1 分类问题1.2 回归问题2. 无监督学习2.1 聚类问题2.2 数据降维3. 半监督学习4. 强化学习三、拟合问题1. 欠拟合2. 过拟合3. 奥卡姆剃刀原则…

Android:aidl简单应用

创建aidl文件&#xff1a;IMyAidlInterface.aidl Build-> Clean Project 重新编译下项目 生成IMyAidlInterface.class文件 interface IMyAidlInterface { void basicTypes(int jk); } --MyService public class MyService extends Service { //定义音乐…