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,一经查实,立即删除!

相关文章

PySide6+VSCode Python可视化环境搭建

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

MySQL 数据库表设计和优化

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

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

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

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

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

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技术——事务和锁机制 一、事务(1)概述(2)ACID特性(3)事务并发存在的问题(4)事务的隔离级别 二、锁机制(1)锁的力度(2)表的分类&…

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

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

公共字段自动填充

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

linux环境安装cuda toolkit

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

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

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

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

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

Linux使用基础命令

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

Linux多线程服务端编程:使用muduo C++网络库 学习笔记 附录B 从《C++ Primer(第4版)》入手学习C++

这是作者为《C Primer(第4版)(评注版)》写的序言,文中“本书”指的是这本书评注版。 B.1 为什么要学习C 2009年本书作者Stanley Lippman先生应邀来华参加上海祝成科技举办的C技术大会,他表示人们现在还用…

扩展学习|大数据分析的现状和分类

文献来源:[1] Mohamed A , Najafabadi M K , Wah Y B ,et al.The state of the art and taxonomy of big data analytics: view from new big data framework[J].Artificial Intelligence Review: An International Science and Engineering Journal, 2020(2):53. 下…

蓝桥杯(3.2)

1209. 带分数 import java.io.*;public class Main {static BufferedReader br new BufferedReader(new InputStreamReader(System.in));static PrintWriter pw new PrintWriter(new OutputStreamWriter(System.out));static final int N 10;static int n, cnt;static int[…

LabVIEW流量控制系统

LabVIEW流量控制系统 为响应水下航行体操纵舵翼环量控制技术的试验研究需求,通过LabVIEW开发了一套小量程流量控制系统。该系统能够满足特定流量控制范围及精度要求,展现了其在实验研究中的经济性、可靠性和实用性,具有良好的推广价值。 项…

tritonserver学习之八:redis_caches实践

tritonserver学习之一:triton使用流程 tritonserver学习之二:tritonserver编译 tritonserver学习之三:tritonserver运行流程 tritonserver学习之四:命令行解析 tritonserver学习之五:backend实现机制 tritonserv…

【C++初阶】内存管理

目录 一.C语言中的动态内存管理方式 二.C中的内存管理方式 1.new/delete操作内置类型 2.new和delete操作自定义类型 3.浅识抛异常 (内存申请失败) 4.new和delete操作自定义类型 三.new和delete的实现原理 1.内置类型 2.自定义类型 一.C语…

C++学习笔记:二叉搜索树

二叉搜索树 什么是二叉搜索树?搜索二叉树的操作查找插入删除 二叉搜索树的应用二叉搜索树的代码实现K模型:KV模型 二叉搜索树的性能怎么样? 什么是二叉搜索树? 二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树: 若它的左子树…

Linux安装Nginx详细步骤

1、创建两台虚拟机,分别为主机和从机,区别两台虚拟机的IP地址 2、将Nginx素材内容上传到/usr/local目录(pcre,zlib,openssl,nginx) 附件 3、安装pcre库   3.1 cd到/usr/local目录 3.2 tar -zxvf pcre-8.36.tar.gz 解压 3.3 cd…