C语言数据结构基础——双链表专题

前言

   书接上回,双链表便是集齐带头、双向、循环等几乎所有元素的单链表PLUS.

1.初始化、创建双链表

typedef int LTDataType;
typedef struct LTNode {LTDataType data;struct LTNode* next;struct LTNode* prev;
}LTNode;

   不同于单链表,此时每个节点应当包含两个指针,一个指向前,一个指向后。

任然将创建节点和初始化双链表封装成两个函数

LTNode* LTBuyNode(LTDataType x) {LTNode* phead = (LTNode*)malloc(sizeof(LTNode));if (phead == NULL) {perror("malloc fail!");exit(1);}phead->data = x;phead->next = phead->prev = NULL;
}LTNode* LTInit() {LTNode* phead = LTBuyNode(-1);phead->next = phead;phead->prev = phead;return phead;
}

      LTInit步骤中,phead便是我们的哨兵位,可以不予其data赋值,也可以赋予一个不太可能成为数据的值。但是我们需要将他的next指针和prev指针分别指向下一个节点(目前是他自己)和上一个节点(目前也是他自己),这样就形成了双链表的雏形

2.插入接口

2.1尾插

void LTNodePushBack(LTNode* phead,LTDataType x) {assert(phead);LTNode* newnode = LTBuyNode(x);//开始调整各个指针指向phead->next = newnode;
}

请各位稍加思考,开始调整指针的第一句对吗?

错了!我们认为此时的双链表只有一个头结点,所以尾差应该插在哨兵位后面,但我们函数的目的是适用于所有的尾插,这便是惯性思维 带来的错误。写各种功能函数时,提前构思出各种情况固然是好事,但对于我们新手与初学者而言,先在脑海中的普通且简答的情况下写出接口,再根据各个特殊情况调整才更加合适。

那我们还需要遍历链表找尾节点吗?

答案是否定的,由于循环链表的缘故,我们可以从头结点(哨兵位)找到现在的尾节点,也就是phead->prev

void LTNodePushBack(LTNode* phead,LTDataType x) {assert(phead);LTNode* newnode = LTBuyNode(x);//开始调整各个指针指向newnode->prev = phead->prev;newnode->next = phead;//先修改新节点的元素的指向,此时不会导致任何节点丢失phead->prev->next = newnode;phead->prev = newnode;
}

打印函数封装如下:

void LTNodePrint(LTNode* phead) {assert(phead);LTNode* pcur = phead->next;while (pcur != phead) {printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}

不同于前面的单链表,此处我们没有再使用二级指针,原因如下:

当链表中只有哨兵位节点时,我们称链表为空链表,无论如何,我们不应该删除的哨兵位。

所以,不同于单链表,双链表一般情况不需要传二级指针

       单链表很多时候设计修改自己的地址,所以需要使用二级指针,而双链表大多数可以直接通过一级指针修改指针指向的内容,不需要使用二级指针。

但比如说,实现删除链表的接口,此时就可以哨兵位的二级指针,因为涉及到修改、删除哨兵位。

不过一级指针也可以使用(只是最后需要手动置NULL),但是可以保证接口一致性,接口一致性能降低客户的使用成本。


再补充一个博主修改双链表指针指向的思路:

1.首先修改要插入节点的本身元素指针的指向。

2.再修改待插入元素的前驱和后驱的指针指向。


2.2头插

    头插是在第一个有效节点之前插入数据,而不是在哨兵位之前插入。哨兵位之前插入数据和尾差无异。尾差才是在最后一个有效节点之后插入数据/哨兵位之前插入数据

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

赋值思路依然如上:先给newnode的next和prev赋值,此时这样操作不会影响任何人,再依次改变前驱和后驱节点的指针指向

3.删除接口

3.1尾删

除了断言哨兵位是否为空,还要断言phead->next!=phead(只剩一个哨兵位也叫空链表,不能再进行删除操作),头删也是这个道理。

void LTNodePopBack(LTNode* phead) {assert(phead);assert(phead != phead->next);LTNode* ptail = phead->prev;ptail->prev->next = phead;phead->prev = ptail -> prev;free(ptail);
}

感觉到指针指向较多怕丢失时,也可以像上面这样定义一个新变量记录地址,也更加容易理解。

3.2头删

    同理,为了不造成空间浪费,我们仍然定义一个新变量来记录想删除的第一个有效节点,方便使用free函数。

void LTNodePopInfront(LTNode* phead) {assert(phead);assert(phead != phead->next);phead->next->next->prev = phead;LTNode* del = phead->next;phead->next = phead->next->next;free(del);del = NULL;
}

4.指定位置的操作

4.1查找接口

    为了便于获得指定位置的操作的实参,我们实现一个查找函数。

LTNode* LTFind(LTNode* phead, LTDataType x) {assert(phead);LTNode* pcur = phead->next;for (; pcur != phead; pcur = pcur->next) {if (pcur->data == x) {return pcur;}}printf("find LTData Failed!");return NULL;
}

4.3指定位置之后插入数据

void LTInsert(LTNode* pos, LTDataType x) {assert(pos);LTNode* newnode = LTBuyNode(x);newnode->prev = pos;newnode->next = pos->next;//先完成newnode的赋值pos->next->prev = newnode;pos->next = newnode;
}

4.4删除指定位置的节点 

理不清楚关系就定义新变量,思路一下就简化了

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

    最后全部的测试的通过了。

5.链表与顺序表的比较和数据结构小结

我们已经学习了两种类型的数据结构,下面进行小结

数据结构是与数据库/文件等价的一门课程,在高校中这两种管理方式也多以单独的课程开放。

那么就我们学习过的顺序表和链表两种结构而言,孰优孰劣呢?

顺序表:

(所谓随机访问并不是真的表示随机,而是说我想访问哪都可以直接访问)

链表(一般不说单链表,而说功能齐全的双链表)

就红字内容,我们再稍微简略的展开说说:

cpu是不会直接从内存中拿取数据的(速度:寄存器>缓存>内存>硬盘),一般情况都是从缓存中拿取数据(数据量小的时候寄存器也可以直接拿数据)。

大部分情况下,如果缓存中有数据,cpu就可以直接“命中”,没有就不命中,先从内存加载到缓存中再命中。

由局部性原理,cpu会一次性的直接去拿一定体量的连续数据(由硬件性质决定)。

而由于顺序表是连续的,比如下图,第一次没能命中,由于已经加载了没有命中的指针所指向的数据及其后面空间的数据,之后都能直接命中,而对于空间不连续的链表,大概率情况下是不会继续命中的,每一次都会经历从内存加载到缓存的过程,降低效率。甚至有可能造成缓存污染,也cpu一次性能装的数据有限,很多有用的数据可能被无效的节点之后的空间挤掉,造成污染。

过程如下:

(cache line为缓存)

在cache line的话直接命中,较高效

不在的话就不命中,去内存中找(比如顺序表是连续的内存,就会很方便找)。

6.小结

存在即合理,在当顺序表和链表没有其他接口的影响时,顺序表的查找会更快。

充分的理解各种数据结构,手撕各种数据结构才能在以后的学习中更方便选型。可参考:

与程序员相关的CPU缓存知识 | 酷 壳 - CoolShell

  

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

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

相关文章

selenium初始学习--打开新标签操作

selenium 打开新标签操作 简单说一下使用 环境 :python 3.9 selenium 4,18 初始化操作 目的 打开bilibilie网站并搜索视频(电影) 并点击观看 操作 打开应用并搜索网址 from selenium import webdriver import timefrom selenium.webdr…

PySide6+VSCode Python可视化环境搭建

#记住在cmd中运行,不要在vscode里运行,否则env会装到工程目录下 python -m venv env #env\Scripts\activate.bat pip install pyside6 下载本期源码 vscode装一个PYQT Integration插件,设置好两个路径(下面有个脚本用于获取路径&…

MySQL 数据库表设计和优化

一、数据结构设计 正确的数据结构设计对数据库的性能是非常重要的。 在设计数据表时,尽量遵循一下几点: 将数据分解为合适的表,每个表都应该有清晰定义的目的,避免将过多的数据存储在单个表中。使用适当的数据类型来存储数据&…

2020小学甲组--恢复数组

题目描述 有一个数组a[1..n]&#xff0c;但是这个数组的内容丢失了&#xff0c;你要尝试恢复它。已知以下的三个事实&#xff1a; 1、对于1<i<n&#xff0c;都有a[i]>0&#xff0c;且所有的a[i]互不相同。即a数组保存的全部都是正整数&#xff0c;且互不相同。 2、…

挑战杯 基于机器视觉的车道线检测

文章目录 1 前言2 先上成果3 车道线4 问题抽象(建立模型)5 帧掩码(Frame Mask)6 车道检测的图像预处理7 图像阈值化8 霍夫线变换9 实现车道检测9.1 帧掩码创建9.2 图像预处理9.2.1 图像阈值化9.2.2 霍夫线变换 最后 1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分…

范伟:你们怎么老提1,200呢,有什么典故啊?赵本山:没有啊!

范伟&#xff1a;你们怎么老提1,200呢,有什么典故啊?赵本山&#xff1a;没有啊&#xff01; --小品《面子》&#xff08;中3&#xff09;的台词 表演者&#xff1a;赵本山 高秀敏 范伟 &#xff08;接上&#xff09; 范伟&#xff1a;哎吃啊 赵&#xff1a;哎呀这电视看的挺…

Acwing枚举、模拟与排序(一)

连号区间数 原题链接&#xff1a;https://www.acwing.com/problem/content/1212/ 初始最小值和最大值的依据是题目给出的数据范围。只要在数据范围之外就可以。 连号的时候&#xff0c;相邻元素元素之间&#xff0c;差值为1。那么区间右边界和左边界&#xff0c;的值的差&#…

cAdvisor+Prometheus+Grafana 搞定Docker容器监控平台

cAdvisorPrometheusGrafana cAdvisorPrometheusGrafana 搞定Docker容器监控平台1、先给虚拟机上传cadvisor2、What is Prometheus?2.1、架构图 3、利用docker安装普罗米修斯4、安装grafana cAdvisorPrometheusGrafana 搞定Docker容器监控平台 1、先给虚拟机上传cadvisor cAd…

MySQL事务和锁机制

MySQL技术——事务和锁机制 一、事务&#xff08;1&#xff09;概述&#xff08;2&#xff09;ACID特性&#xff08;3&#xff09;事务并发存在的问题&#xff08;4&#xff09;事务的隔离级别 二、锁机制&#xff08;1&#xff09;锁的力度&#xff08;2&#xff09;表的分类&…

网络编程-编码与解码(Protobuf)

编码与解码 下面的文字都来自于极客时间 为什么要编解码呢&#xff1f;因为计算机数据传输的是二进制的字节数据 解码&#xff1a;字节数据 --> 字符串&#xff08;字符数据&#xff09; 编码&#xff1a;字符串&#xff08;字符数据&#xff09;–> 字节数据 我们在编…

Python 实现海康机器人工业相机 MV-CS050-10GC 的实时显示视频流及拍照功能(实时显示视频流同时可以进行拍照)

参考链接&#xff1a; https://www.cnblogs.com/HanYork/p/17388506.html https://www.cnblogs.com/miracle-luna/p/16960556.html#5138211 Flask搭建流媒体服务器&#xff1a;使用Flask搭建一个流媒体服务器_multipart/x-mixed-replace; boundaryframe-CSDN博客

公共字段自动填充

在开发中经常面临对于一些公共字段的赋值。 如在下表中&#xff1a; 如何让程序自动为我们需要赋值的公共字段进行赋值&#xff0c;避免在业务代码中重复写这些公共字段的赋值代码 如下图所示&#xff1a; 实现思路&#xff1a; 1.自定义注解AutoFill&#xff0c;用于标识需…

linux环境安装cuda toolkit

1 全新安装 如果环境中没安装过cuda版本&#xff0c; 这种情况下比较简单。 直接在https://developer.nvidia.com/cuda-toolkit-archive选择对应版本下载安装即可。 如下为安装cuda toolkit 11.8. 2 环境中已经存在其他版本 这种情况下比较复杂一些。 首先要确认最高支持的…

李沐动手学习深度学习——4.2练习

1. 在所有其他参数保持不变的情况下&#xff0c;更改超参数num_hiddens的值&#xff0c;并查看此超参数的变化对结果有何影响。确定此超参数的最佳值。 通过改变隐藏层的数量&#xff0c;导致就是函数拟合复杂度下降&#xff0c;隐藏层过多可能导致过拟合&#xff0c;而过少导…

Git多人合作的推送流程

多人合作时&#xff0c;使用Git进行代码推动&#xff08;push&#xff09;需要一定的协调和规范&#xff0c;以确保代码库的整体健康。以下是一个常见的多人合作时的Git代码推动流程&#xff1a; 同步主分支&#xff1a; 在推送之前&#xff0c;确保你的本地主分支&#xff08;…

【Java】四大函数式接口

消费型接口Consumer 消费型接口接收一个输入&#xff0c;没有返回值 在stream流计算中 forEach() 接收一个消费型接口Consumer用于 遍历元素 /*** 消费型接口* 接收一个输入&#xff0c;没有返回值*/ public class demo01 {public static void main(String[] args) {//TODO 消…

【MySQL】表的内连和外连(重点)

表的连接分为内连和外连。 一、内连接 内连接实际上就是利用 where 子句对两种表形成的笛卡儿积进行筛选&#xff0c;前面学习的查询都是内连接&#xff0c;也是在开发过程中使用的最多的连接查询。 select 字段 from 表1 inner join 表2 on 连接条件 and 其他条件; 注意&…

【数仓】Hadoop集群配置常用参数说明

Hadoop集群中&#xff0c;需要配置的文件主要包括四个 配置核心Hadoop参数&#xff1a; 编辑core-site.xml文件&#xff0c;设置Hadoop集群的基本参数&#xff0c;如文件系统、Hadoop临时目录等。 配置HDFS参数&#xff1a; 编辑hdfs-site.xml文件&#xff0c;设置HDFS的相关参…

策略开发:EMA如何计算

EMA的计算原理 EMA 是MA&#xff08;平滑移动平均线&#xff09;的另一种形式。全名“加权指数移动平均线”。 2/13就是12日移动平均线的平滑因子&#xff0c;他的意思是指&#xff1a;给予新价格 2/13的权重&#xff0c;给予过去的EMA 11/13的权重。 在计算的时候第一天的M…

Linux使用基础命令

1.常用系统工作命令 (1).用echo命令查看SHELL变量的值 qiangziqiangzi-virtual-machine:~$ echo $SHELL /bin/bash(2).查看本机主机名 qiangziqiangzi-virtual-machine:~$ echo $HOSTNAME qiangzi-virtual-machine (3).date命令用于显示/设置系统的时间或日期 qiangziqian…