【数据结构入坑指南(四.2)】--《从单链到双链的进阶,读懂“双向奔赴”的算法之美与效率权衡》

【数据结构入坑指南(四.2)】--《从单链到双链的进阶,读懂“双向奔赴”的算法之美与效率权衡》

🔥@晨非辰Tong:个人主页 

👀专栏:《C语言》《数据结构与算法》《数据结构与算法刷题集》

💪学习阶段:C语言、数据结构与算法初学者

⏳“人理解迭代,神理解递归。”


引言:征服了单链表,却总感束缚?无法回溯的遍历、繁琐的节点删除...是时候解锁双向链表,体验指针自由穿梭的真正力量。

目录

一、链表的分类、说明

1.1  带头、不带头

1.2  单向、双向

1.3  循环、不循环

二、双向链表

2.1  基本信息介绍

2.1  定义、结构

2.2  “哨兵位”的初始化(准备工作)

2.2  链表基本功能实现

2.2.1  双链表尾插

2.2.2  双链表头插

2.2.3  双链表尾删

 2.2.4  双链表头删


一、链表的分类、说明

--根据表格知道:链表的组合共有8种。那么具体是什么?

1.1  带头、不带头

--“头节点”:一般是指不存储任何有效数据的节点。

--在前面的学习中,也说过“头节点”或者“首节点”,但所谓的节点是存储着有效数据的节点,是为了好说明

--这里的头指的是前面刷题提到过的“哨兵位”——>不存储任何有效数据,只是用来指向的第一个节点(存储有效数据)。

1.2  单向、双向

--对比可以看到,双向链表比单向链表多一个指针(称为前驱指针)——>存储上一个节点地址,都有的指针在双链表称为后继指针。

--对于这样三个指针的节点结构:1指针-->前驱指针,2指针-->存储数据值,3指针-->后继指针。

1.3  循环、不循环

--对于前面所学单链表,到了尾节点就会指向NULL——>这就是不循环。

--但是对于循环的链表——>尾节点的后继指针(next)会指向第一个节点。(这样循环往复)

虽然说了有8种链表,但是常用的组合为:不带头单向不循环链表 和 带头双向循环链表。

--下面见识一下双向链表。

二、双向链表

2.1  基本信息介绍

2.1  定义、结构

所谓双向链表:就如上面图示一样——>首先有头节点,然后存在着前驱指针指向前一个节点-->这就是双向,最后尾节点的后继指针指向头节点形成循环。
DList.h文件 #include <stdio.h> #include <stdlib.h> //定义双链表基本结构(节点) typedef int DLTDataType; typedef struct DListNode { DLTDataType data; struct DListNode* next;//指向下一个节点 struct DListNode* prev;//指向前一个节点 }DLTNode;

2.2  “哨兵位”的初始化(准备工作)

--在实现链表的功能时,首先实现空链表:先将“哨兵位”的结构整出来。

双链表最开始为空,意味着“哨兵节点”始终指向自己(不是连“哨兵节点”都没有)——>前驱指针、后继指针都指向头节点本身。
DList.c文件 #include "DList.h" //定义_初始化 void DLTInit(DLTNode** pphead)//二级指针接收“哨兵位”地址-->改变结构(test.c传的头节点) { //申请空间 *pphead = (DLTNode*)malloc(sizeof(DLTNode));//*pphead 是 phead //判断非空 if (*pphead == NULL) { perror("malloc"); exit(1);//申请失败 } (*pphead)->data = -1;//随便一个值,后续不用 (*pphead)->next = (*pphead)->prev = *pphead;//指向自己 }

--“哨兵节点”初始化时的数据指针存储什么数据?

        :随便放什么值,以后也不会用到 head->data。

test.c文件 #include "DList.h" //测试 void test01() { DLTNode* plist = NULL;//不是双向链表,初始化 //初始化 DLTInit(&plist);//传址 } int main() { test01(); return 0; }

--可以看到:因为是传址调用,实参(plist)与形参(*pphead)节点结构一致,初始化成功。

2.2  链表基本功能实现

2.2.1  双链表尾插

--要尾插新节点,就要改变多处指针的指向。

--进行改变,先对newnode部分进行(若先变head、d3,导致指向变化,newnode无从下手),两图对照写。

--先将创建新节点定义出来:封装函数

DList.c文件 #include "DList.h" //定义_由数值创建新节点 DLTNode* DLTBuyNode(DLTDataType x) { //申请空间 DLTNode* newnode = (DLTNode*)malloc(sizeof(DLTNode)); //判断非空 if (newnode == NULL) { perror("malloc"); exit(1);//申请失败 } newnode->data = x; newnode->next = newnode->prev = newnode;//新节点先指向自己 return newnode; } 

--新申请的节点,刚开始前驱指针、后继指针都要指向自己,在后续的指向改变中完成变化。

DList.c文件 #include "DList.h" //定义_尾插 void DLTPushBack(DLTNode* phead, DLTDataType x)//一级指针,只需要知道是哪个链表 { //断言 assert(phead); //创建新节点 DLTNode* newnode = DLTBuyNode(x); //先对newnode进行改变,防止phead、尾节点指向改变 newnode->prev = phead->prev; newnode->next = phead; phead->prev->next = newnode;//->prev->next代表尾节点next phead->prev = newnode; } 
test.c文件 #include "DList.h" void test01() { DLTNode* plist = NULL;//不是双向链表,初始化 //初始化 DLTInit(&plist);//传址 //尾插 DLTPushBack(plist, 1); DLTPushBack(plist, 2); DLTPushBack(plist, 3); DLTPushBack(plist, 4); } int main() { test01(); return 0; }

2.2.2  双链表头插

--在头节点后,第一个节点前进行插入。

对于关于d1节点相关指针的改变,通过 head 头节点的相关指针进行改变。
DList.c文件 #include "DList.h" //定义_头插 void DLTPushFront(DLTNode* phead, DLTDataType x) { //断言 assert(phead); //申请新节点 DLTNode* newnode = DLTBuyNode(x); newnode->next = phead->next; newnode->prev = phead; phead->next->prev = newnode; phead->next = newnode; }
test.c文件 #include "DList.h" void test01() { DLTNode* plist = NULL;//不是双向链表,初始化 //初始化 DLTInit(&plist);//传址 //头插 DLTPushFront(plist, 1); DLTPushFront(plist, 2); DLTPushFront(plist, 3); DLTPushFront(plist, 4); } int main() { test01(); return 0; }

2.2.3  双链表尾删

 --在实现尾删操作前,先来定义判空的函数

//定义-判空 bool DLTEmpty(DLTNode* phead) { assert(phead); return phead->next == phead; }

--对于打印链表:为了体现尾删、头删的效果,将链表进行打印

//定义_打印 void DLTPrint(DLTNode* phead) { DLTNode* pcur = phead->next;//指向第一个节点 //循环遍历 while (pcur != phead) { printf("%d->", pcur->data); pcur = pcur->next; } printf("\n"); }

--对于尾删:

尾删操作:将头节点及尾节点的上一个节点的前驱指针、后继指针改变指向,都不再指向尾节点。
DList.c文件 #include "DList.h" //定义_尾删 void DLTPopBack(DLTNode* phead) { assert(!DLTEmpty(phead)); //首先定义指针指向尾节点 DLTNode* del = phead->prev; del->prev->next = phead; phead->prev = del->prev; //对del节点空间进行释放 free(del); del = NULL; }
test.c文件 #include "DList.h" void test01() { DLTNode* plist = NULL;//不是双向链表,初始化 //初始化 DLTInit(&plist);//传址 //尾插 DLTPushBack(plist, 1); DLTPushBack(plist, 2); DLTPushBack(plist, 3); DLTPushBack(plist, 4); DLTPrint(plist); //尾删 DLTPopBack(plist); DLTPrint(plist); DLTPopBack(plist); DLTPrint(plist); DLTPopBack(plist); DLTPrint(plist); DLTPopBack(plist); DLTPrint(plist); } int main() { test01(); return 0; }

 2.2.4  双链表头删

--对于头删的操作,画图弄清楚节点的前驱指针、后继指针的指向关系,即将头节点、第二个节点的前驱指针、后继指针不在指向第一个节点。
DList.c文件 #include "DList.h" //定义_头删 void DLTPopFront(DLTNode* phead) { assert(!DLTEmpty(phead)); //定义指针指向第1个节点 DLTNode* del = phead->next; phead->next = del->next; del->next->prev = phead; //释放del free(del); del = NULL; }
先定义指针指向第一个节点,再根据图示,改变头节点后继指针指向第二个节点,改变第二个节点的前驱指针指向头节点。
test.c文件 #include "DList.h" void test01() { DLTNode* plist = NULL;//不是双向链表,初始化 //初始化 DLTInit(&plist);//传址 //尾插 DLTPushBack(plist, 1); DLTPushBack(plist, 2); DLTPushBack(plist, 3); DLTPushBack(plist, 4); DLTPrint(plist); //头删 DLTPopFront(plist); DLTPrint(plist); DLTPopFront(plist); DLTPrint(plist); DLTPopFront(plist); DLTPrint(plist); DLTPopFront(plist); DLTPrint(plist); } int main() { test01(); return 0; }

回顾:

《面试高频数据结构:单链表与顺序表之争,读懂“不连续”之美背后的算法思想》

《链表面试基础看点:这里不止“快慢指针”的完美实现,更懂“哨兵节点”的巧妙运用》
结语:从单链表的“单向枷锁”中解脱出来,我们初步领略了双向链表在头尾操作上的简洁与高效。但它的威力远不止于此。如何利用双指针的便利,实现比单链表更优雅、更高效的任意位置操作?这一切的背后,又隐藏着哪些不容错过的细节陷阱?下一篇,我们将拨开迷雾,一探究竟。

Read more

速通前端篇 —— HTML

速通前端篇 —— HTML

找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程程(ಥ_ಥ)-ZEEKLOG博客 所属专栏:速通前端 目录 HTML的介绍 如何创建HTML文件 HTML 文件基本结构 HTML常用标签 title标签   标题标签 h1-h6  段落标签 p 换行标签 br 图片标签 img  超链接 a 表格标签 table 表单标签 input 标签 form 标签  select 标签 textarea 标签  无语义标签 div&span 列表标签  综合练习:用户登录  由于我们Java是属于后端开发的,因此对于前端部分,我们只需要简单了解,达到认识与编写基本的代码即可。  HTML的介绍 HTML(Hyper

By Ne0inhk
Linux网络 | 理解Web路径 以及 实现一个简单的helloworld网页

Linux网络 | 理解Web路径 以及 实现一个简单的helloworld网页

前言:本节内容承接上节课的http相关的概念, 主要是实现一个简单的接收http协议请求的服务。这个程序对于我们理解后面的http协议的格式,报头以及网络上的资源的理解, 以及本节web路径等等都有着重要作用。 可以说我们就用代码来理解这些东西。 那么废话不多说, 现在开始我们的学习吧。         ps:本节内容建议先看一下上一篇文章http的相关概念哦:linux网络 | 深度学习http的相关概念-ZEEKLOG博客 目录  准备文件  makefile HttpServer.hpp 类内成员 封装sockfd start  ThreadRun  全部代码 运行结果 响应书写 Web路径  准备文件         首先准备文件: 这里面Httpserver.cc用来运行接收http请求的服务。 HttpServer.hpp用来定义http请求。Log.hpp就是一个打印日志的小组件, Socket.hpp同样是套接字的组件。 到使用直接调用相关接口即可。(Log.hpp和Socket.hpp如何实现不讲解, 如果想要知道

By Ne0inhk
Flutter for OpenHarmony:web3dart 连接以太坊区块链,构建去中心化应用(DApp 开发与智能合约调用深度实战)深度解析与鸿蒙适配指南

Flutter for OpenHarmony:web3dart 连接以太坊区块链,构建去中心化应用(DApp 开发与智能合约调用深度实战)深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 随着 Web3.0 概念的普及,区块链技术已从早期的极客玩具逐渐走向主流应用。无论是 DeFi(去中心化金融)、NFT(非同质化代币)还是 DAO(去中心化组织),都离不开与区块链网络的交互。 以太坊 (Ethereum) 作为目前最成熟的智能合约平台,其客户端通信协议 JSON-RPC 是行业标准。要在移动端(Flutter/OpenHarmony)与以太坊网络通信,我们不可能手动构造那些复杂的十六进制数据包。 web3dart 是 Dart 生态中唯一的、功能完备的 Web3 客户端库。它可以让你: 1. 管理账户:生成私钥、助记词,导入 keystore。 2. 发送交易:转账 ETH,部署合约。

By Ne0inhk

本地部署更安全:GLM-4.6V-Flash-WEB保护数据隐私

本地部署更安全:GLM-4.6V-Flash-WEB保护数据隐私 在企业数字化转型加速的当下,越来越多业务场景依赖图文联合理解能力——客服截图自动诊断、电商商品图智能打标、教育习题拍照解析、医疗报告图像辅助生成……这些需求背后,都指向同一个关键前提:图像与文本必须被同一个模型“看懂”并“说清”。 但现实中的落地障碍始终清晰可见:调用公有云多模态API,意味着用户上传的图片、对话记录、业务截图等敏感数据将离开本地环境;而自建服务又常面临高门槛——动辄需要A100×4集群、复杂环境配置、数小时部署调试,甚至还要担心模型权重是否真正开源、能否审计代码逻辑。 此时,GLM-4.6V-Flash-WEB 的出现,提供了一条截然不同的路径:它不是云端黑盒,也不是实验室Demo,而是一个开箱即用、单卡可跑、全程可控的本地化多模态推理引擎。更重要的是,它把“数据不出域”从安全合规要求,变成了技术上自然达成的结果。 本文不讲参数对比,不堆SOTA指标,只聚焦一个核心问题:如何用最简单的方式,在你自己的机器上,跑起一个真正能保护数据隐私的图文理解服务? 1. 为什么本地部署

By Ne0inhk