【探索数据结构与算法】——深入了解双向链表(图文详解)
目录
一、双向链表的基本概念
二、双向链表的结构
三、双向链表的基本操作实现方法
1.双向链表的初始化
2.双向链表的头插
3.双向链表的尾插
6.查找节点
7.在指定位置之前插入节点
8.删除指定位置节点
9.打印链表数据
10.双向链表销毁
四、完整代码实现
LIst.h
List.c
💓 博客主页:C-SDN花园GGbond
⏩ 文章专栏:探索数据结构与算法
一、双向链表的基本概念
双向链表是一种常见的数据结构,与单向链表类似,但它允许我们从两个方向遍历链表:向前和向后。每个节点包含三个部分:一个数据元素和两个指针,一个指向链表中的前一个节点,另一个指向链表中的下一个节点。
一般情况下,我们所说的双向链表指的是带头节点,双向,循环链表,以下若无特殊说明,均代表此含义。
二、双向链表的结构
双向链表中的每个节点通常包含以下部分:
- 数据元素:可以是任何类型的数据,如整数、浮点数、字符串或对象。
- prev 指针:指向前一个节点的指针。
- next 指针:指向下一个节点的指针。相比单链表只有独立存在的每个节点,双向链表多了哨兵位节点,该节点作为头结点,不存储有效数据,只有指向第一个有效节点的next指针和指向尾节点的prev指针。
只要链表存在,哨兵位节点就存在
相比单链表只有独立存在的每个节点,双向链表多了哨兵位节点,该节点作为头结点,不存储有效数据,只有指向第一个有效节点的next指针和指向尾节点的prev指针。
只要链表存在,哨兵位节点就存在
三、双向链表的基本操作实现方法
- 初始化顺序表中的数据。
- 对顺序表进行头插(开头插入数据)。
- 对顺序表进行尾插插(末尾插入数据)。
- 对顺序表进行头删(开头删除数据)。
- 对顺序表进行尾删(末尾删除数据)。
- 对顺序表进行查找。
- 7.在指定位置之前插入节点
- 删除指定位置节点。
- 打印顺序表中的数据。
- .双向链表销毁
1.双向链表的初始化
双向链表的初始化也就是创建一个哨兵位节点,所以实现初始化之前需要先封装一个节点申请函数
单独封装的申请节点函数
- 动态申请节点大小的空间
- 判空
- 通过形参接受的数据初始化节点数据部分
- next指针和prev指针都指向自身(因为双向链表是循环链表)
- 返回节点地址
LTNode* NewNode(LTDataType x)//申请节点
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("NewNode\n");exit(1);}newnode->data = x;//根据参数初始化数据newnode->next = newnode->prev = newnode;//两个指针初始都指向自身return newnode;
}
双向链表初始化
- 调用申请节点函数创建一个哨兵位节点,作为链表的头结点
- 返回链表头结点地址
注意:
双向链表因为有哨兵位的存在,链表始终不为空,哨兵位节点不会被改变,所以不再需要传递地址,以及用二级指针接收。这一点区别于单链表实现
LTNode* LTInit() //链表初始化
{LTNode* newnode = NewNode(-1);return newnode;
}
2.双向链表的头插
双向链表头插
- 头插就是将新节点插入到哨兵位节点和第一个有效节点之间
- 形参接收一个哨兵位地址和要插入的数据
- 首先对哨兵位地址判空,判断链表结构是否正常
申请新节点,存入要插入的数据
接下来是移动指针
一般情况下应当先调整新节点的指针,因为新节点指针调整不会影响到原链表:
新节点prev指向哨兵位,next指向哨兵位的next所指向节点
然后,调整第一个有效节点的prev指针,不再指向哨兵位而是指向新节点;调整哨兵位
next指针,不再指向原来的第一个有效节点,而是指向新节点
新节点插入完成
void LTPushFront(LTNode* phead, LTDataType x)//头插
{assert(phead);//判空LTNode* newnode = NewNode(x);//申请节点newnode->next = phead->next;//改变新节点指针指向newnode->prev = phead;phead->next->prev = newnode;//改变原链表相关节点指针指向phead->next = newnode;}
3.双向链表的尾插
双向链表尾删
- 尾删就是将双向链表的最后一个有效节点删除
- (尾删的前提是至少存在一个有效节点)
- 首先创建一个指针del指向要删除的节点(哨兵位prev指向节点)
- 然后将倒数第二个节点的next指针指向哨兵位
- 将哨兵位的prev指针指向倒数第二个节点
- 指针修改完成,此时倒数第二个节点成为最后一个节点,释放del指向的节点
- (如果删除之前链表只有一个节点,删除完之后只剩下一个哨兵位节点,两个指针都指向自己)
void LTPopBack(LTNode* phead) //尾删
{assert(phead && phead->next != phead);//判空LTNode* del = phead->prev;//暂时存储要删除的节点del->prev->next = phead;//移动指针phead->prev = del->prev;free(del);//释放节点空间del = NULL;
}
4.双向链表的头删
双向链表头删
- 头删就是将双向链表第一个有效节点删除
- (头删的前提是链表至少存在一个有效节点)
- 首先创建一个指针del指向要删除的节点(哨兵位节点next指向的节点)
- 然后将第二个有效节点的prev指针指向哨兵位节点
- 哨兵位节点的next指针指向第二个有效节点
- 指针修改完成,此时第二个有效节点成为链表的第一个有效节点,释放del指向的节点
- (如果删除之前链表只有一个节点,删除完之后只剩下一个哨兵位节点,两个指针都指向自己)
void LTPopFront(LTNode* phead) //头删
{assert(phead && phead->next != phead);//判空LTNode* del = phead->next;//暂时存储要删除的节点del->next->prev = phead;//移动指针phead->next = del->next;free(del);//释放节点空间del = NULL;
}
5.双向链表的尾删
双向链表尾删
- 尾删就是将双向链表的最后一个有效节点删除
- (尾删的前提是至少存在一个有效节点)
- 首先创建一个指针del指向要删除的节点(哨兵位prev指向节点)
- 然后将倒数第二个节点的next指针指向哨兵位
- 将哨兵位的prev指针指向倒数第二个节点
- 指针修改完成,此时倒数第二个节点成为最后一个节点,释放del指向的节点
- (如果删除之前链表只有一个节点,删除完之后只剩下一个哨兵位节点,两个指针都指向自己)
void LTPopBack(LTNode* phead) //尾删
{assert(phead && phead->next != phead);//判空LTNode* del = phead->prev;//暂时存储要删除的节点del->prev->next = phead;//移动指针phead->prev = del->prev;free(del);//释放节点空间del = NULL;
}
6.查找节点
双向链表查找(根据数据查找节点)
- 首先对哨兵位地址判空,否则可能对空地址解引用
- 创建一个遍历链表的指针,从第一个有效节点开始,将节点数据与要查找的数据进行比对,如果相同返回节点地址
- 出循环,说明未找到,返回NULL
LTNode* LTFind(LTNode* phead, LTDataType x)//查找节点
{assert(phead);//判空LTNode* pcur = phead->next;//遍历链表的指针while (pcur!= phead){if (pcur->data == x)return pcur;pcur = pcur->next;}return NULL;
}
7.在指定位置之前插入节点
双向链表在指定位置之前插入节点:
插入的前提是指定位置存在
申请新节点,存入要插入的数据
调整指针:
临时创建一个指针prev指向指定位置节点的perv指向节点
新节点next指针指向指定位置节点,prev指针指向prev节点
然后再修改原链表指针:
指定位置节点的prev指针指向新节点,prev节点的next指针指向新节点
新节点插入完成
void LTInsert(LTNode* pos, LTDataType x)//在pos位置之前插入数据
{assert(pos);//判空LTNode* newnode = NewNode(x);//申请新节点newnode->next = pos;//改变新节点指针指向newnode->prev = pos->prev;pos->prev->next = newnode;//改变原链表相关节点指针指向pos->prev = newnode;}
8.删除指定位置节点
双向链表删除指定位置节点
- 删除指定节点的前提是该节点必须存在
- 首先创建一个指针del指向要删除的节点
- 然后将该节点的前一个结点的next指针指向它的后一个节点,后一个节点的prev指针指向他的前一个节点
- 释放该节点
- (如果删除之前链表只有一个节点,删除完之后只剩下一个哨兵位节点,两个指针都指向自己)
void LTErase(LTNode* pos)//删除指定位置节点
{assert(pos);//判空pos->next->prev = pos->prev;//改变要删除节点的前后节点指针指向pos->prev->next = pos->next;free(pos);pos = NULL;}
9.打印链表数据
双向链表数据打印
- 首先对地址判空,防止对空地址解引用
- 创建一个遍历链表的指针,从第一个有效节点打印数据,然后向后移动,直到指针遍历到哨兵位
void LTPrint(LTNode* phead) //链表数据打印
{assert(phead);//判空LTNode* pcur = phead->next;//遍历链表的指针while (pcur != phead)//打印数据{printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}
10.双向链表销毁
双向链表的销毁
从第一个有效节点开始,创建指针循环遍历链表:
创建next指针临时保存下一个节点,释放本节点,遍历指针指向next节点
循环至有效节点全部被释放
然后将哨兵位节点释放,指针置空,销毁完成
注意:为了与其他配套函数的参数保持一致,这里的参数本该用二级指针接收,却用一级指针接收。出函数后,需要手动将哨兵位指针置空
oid LTDestroy(LTNode* phead)//链表销毁
{assert(phead);//判空LTNode* pcur = phead->next;//遍历链表的指针while (pcur != phead)//从第一个有效节点开始,逐个释放节点{LTNode* next = pcur->next;free(pcur);pcur = next;}free(phead);//释放哨兵位节点phead = NULL;}
四、完整代码实现
LIst.h
//List.h 双链表头文件
#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int LTDataType;//数据类型重定义,可以是任意数据类型
typedef struct ListNode//双向链表节点结构
{LTDataType data;//数据struct ListNode* prev;//指向前一个节点的指针struct ListNode* next;//指向下一个节点的指针
}LTNode;LTNode* NewNode(LTDataType x);//申请节点
LTNode* LTInit(); //链表初始化void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPushBack(LTNode* phead, LTDataType x);//尾插void LTPopBack(LTNode* phead); //尾删
void LTPopFront(LTNode* phead); //头删LTNode* LTFind(LTNode* phead, LTDataType x);//查找节点void LTInsert(LTNode* pos, LTDataType x);//在pos位置之后插入数据
void LTErase(LTNode* pos);//删除指定位置节点void LTPrint(LTNode* phead); //链表数据打印
void LTDestroy(LTNode* phead);//链表销毁
List.c
//DouSList.c 双链表源文件
#define _CRT_SECURE_NO_WARNINGS 1
#include"DouSList.h"LTNode* NewNode(LTDataType x)//申请节点
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("NewNode\n");exit(1);}newnode->data = x;//根据参数初始化数据newnode->next = newnode->prev = newnode;//两个指针初始都指向自身return newnode;
}
LTNode* LTInit() //链表初始化
{LTNode* newnode = NewNode(-1);return newnode;
}
void LTPushFront(LTNode* phead, LTDataType x)//头插
{assert(phead);//判空LTNode* newnode = NewNode(x);//申请节点newnode->next = phead->next;//改变新节点指针指向newnode->prev = phead;phead->next->prev = newnode;//改变原链表相关节点指针指向phead->next = newnode;}
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{assert(phead);//判空LTNode* newnode = NewNode(x);//申请节点newnode->next = phead;//改变新节点指针指向newnode->prev = phead->prev;phead->prev->next = newnode;//改变原链表相关节点指针指向phead->prev = newnode;
}void LTPopFront(LTNode* phead) //头删
{assert(phead && phead->next != phead);//判空LTNode* del = phead->next;//暂时存储要删除的节点del->next->prev = phead;//移动指针phead->next = del->next;free(del);//释放节点空间del = NULL;
}void LTPopBack(LTNode* phead) //尾删
{assert(phead && phead->next != phead);//判空LTNode* del = phead->prev;//暂时存储要删除的节点del->prev->next = phead;//移动指针phead->prev = del->prev;free(del);//释放节点空间del = NULL;
}LTNode* LTFind(LTNode* phead, LTDataType x)//查找节点
{assert(phead);//判空LTNode* pcur = phead->next;//遍历链表的指针while (pcur!= phead){if (pcur->data == x)return pcur;pcur = pcur->next;}return NULL;
}void LTInsert(LTNode* pos, LTDataType x)//在pos位置之前插入数据
{assert(pos);//判空LTNode* newnode = NewNode(x);//申请新节点newnode->next = pos;//改变新节点指针指向newnode->prev = pos->prev;pos->prev->next = newnode;//改变原链表相关节点指针指向pos->prev = newnode;}void LTErase(LTNode* pos)//删除指定位置节点
{assert(pos);//判空pos->next->prev = pos->prev;//改变要删除节点的前后节点指针指向pos->prev->next = pos->next;free(pos);pos = NULL;}void LTPrint(LTNode* phead) //链表数据打印
{assert(phead);//判空LTNode* pcur = phead->next;//遍历链表的指针while (pcur != phead)//打印数据{printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}void LTDestroy(LTNode* phead)//链表销毁
{assert(phead);//判空LTNode* pcur = phead->next;//遍历链表的指针while (pcur != phead)//从第一个有效节点开始,逐个释放节点{LTNode* next = pcur->next;free(pcur);pcur = next;}free(phead);//释放哨兵位节点phead = NULL;}
相关文章:

【探索数据结构与算法】——深入了解双向链表(图文详解)
目录 一、双向链表的基本概念 二、双向链表的结构 三、双向链表的基本操作实现方法 1.双向链表的初始化 2.双向链表的头插 3.双向链表的尾插 6.查找节点 7.在指定位置之前插入节点 8.删除指定位置节点 9.打印链表数据 10.双向链表销毁 四、完整代码实现 …...
linux常用命令备忘录
一、常用命令 查看被占用进程:ps ef|grep 11612 查看当前目录:pwd 查看文件的md5: (linux)md5sum 文件名 (windows)certutil -hashfile some_file MD5 查看当前目录的文件大小:…...

【C++进阶学习】第十二弹——C++ 异常处理:深入解析与实践应用
前言: 在C编程语言中,异常处理是一种重要的机制,它允许程序员在运行时捕获和处理错误或异常情况。本文将详细介绍C异常处理的相关知识点,包括异常的定义、抛出与捕获、异常处理的原则、以及在实际编程中的应用。 目录 1. 异常处理…...
《算法竞赛进阶指南》0x23剪枝
剪枝,就是减少搜索树的规模、尽可能排除搜索书中不必要的分支的一种手段。形象地看,就好像剪掉了搜索树的枝条,故被称为“剪枝”。在深度优先搜索中,有以下常见的剪枝方法。 1.优化搜索顺序 在一些搜索问题中,搜索树的…...

同态加密和SEAL库的介绍(三)BFV - Batch Encoder
写在前面: 在上一篇中展示了如何使用 BFV 方案执行一个非常简单的计算。该计算在 plain_modulus 参数下进行,并且仅使用了 BFV 明文多项式中的一个系数。这种方法有两个显著的问题: 实际应用通常使用整数或实数运算,而不是模运算…...

Docker 环境下使用 Traefik v3 和 MinIO 快速搭建私有化对象存储服务
上一篇文章中,我们使用 Traefik 新版本完成了本地服务网关的搭建。接下来,来使用 Traefik 的能力,进行一系列相关的基础设施搭建吧。 本篇文章,聊聊 MinIO 的单独使用,以及结合 Traefik 完成私有化 S3 服务的基础搭建…...

玛雅房产系统源码开发与技术功能解析
引言 随着房地产市场的蓬勃发展,房产管理系统(Real Estate Management System, REMS)作为提升行业效率、优化资源配置的关键工具,其重要性日益凸显。房产系统源码开发不仅涉及复杂的业务逻辑处理,还融合了先进的软件开…...

c++----初识模板
大家好,这篇博客想与大家分享一些我们c中比较好用的知识点。模板。首先咧,我们都知道模板嘛,就是以前人的经验总结出来的知识。方便我们使用。这里的模板也是一样的。当我们学习过后,对于一些在c中的自定义函数,我们在…...

SpringBoot3热部署
引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional> </dependency> 默认就是,无需配置 可以了…...

J. 二进制与、平方和
https://codeforces.com/gym/104095/problem/J 分析操作一 1&00 ,0&10,ai<qmi(2,24),说明每个数最多操作25次 维护区间或和,orsum & x orsum 就不用递归下去了 势能线段树code // Problem: J. 二进制与、平方和 // Contest: Codeforc…...

LVS中NAT模式和DR模式实战讲解
1DR模式 DR:Direct Routing,直接路由,LVS默认模式,应用最广泛,通过为请求报文重新封装一个MAC首部进行 转发,源MAC是DIP所在的接口的MAC,目标MAC是某挑选出的RS的RIP所在接口的MAC地址;源 IP/PORT…...

写给小白程序员的一封信
文章目录 1.编程小白如何成为大神?大学新生的最佳入门攻略2.程序员的练级攻略3.编程语言的选择4.熟悉Linux5.学会git6.知道在哪寻求帮助7.多结交朋友8.参加开源项目9.坚持下去 1.编程小白如何成为大神?大学新生的最佳入门攻略 编程已成为当代大学生的必…...

Leaf分布式ID
文章目录 系统对Id号的要求UUIDsnowflakeLeafLeaf-snowflakeLeaf-segmentMySQL自增主键segment双buffer 系统对Id号的要求 1、业务 1)全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求 2)趋势递增&a…...

Starrocks解析json数组
json数据 [{"spec": "70g/支","unit": "支","skuId": "1707823848651276346","amount": 6,"weight": 70,"spuName": "伊利 甄稀 苦咖啡味雪糕 流心冰淇淋 70g/支",&quo…...

安卓基本布局(下)
TableLayout 常用属性描述collapseColumns设置需要被隐藏的列的列号。shrinkColumns设置允许被伸缩的列的列号。stretchColumns设置允许被拉伸的列的列号。 <TableLayout xmlns:android"http://schemas.android.com/apk/res/android"android:id"id/TableL…...

Python中使用正则表达式
摘要: 正则表达式,又称为规则表达式,它不是某种编程语言所特有的,而是计算机科学的一个概念,通常被用来检索和替换某些规则的文本。 一.正则表达式的语法 ①行定位符 行定位符就是用来描述字符串的边界。"^&qu…...

三大口诀不一样的代码,小小的制表符和换行符玩的溜呀
# 小案例,打印输出加法口诀 for i in range(1,10):for j in range(1,10):if j>i:breakprint(f"{j}{i}{ji}".strip(),end\t)print() print(\n) for i in range(1,10):for j in range(1,10):if j>i:breakprint(f"{j}x{i}{j*i}",end\t)print…...
[qt] 线程等待与唤醒
对于生产者与消费者的数据处理的另一种好的解决方法是使用QWaitCondition类,允许线程在一定的条件下唤醒其他多个线程来共同处理。 一 定义公共变量 DataSize: 生产者生产数据的大小BufferSize: 也就是这个缓冲区的大小,每个单元是一个int,也有可能是一个链表,结构…...
Springboot 实现 Modbus Rtu 协议接入物联网设备
Modbus RTU 技术教程 引言 Modbus是一种开放标准的通信协议,它最初由Modicon(现施耐德电气)在1979年发布,旨在让可编程逻辑控制器(PLC)之间能够进行通信。随着时间的发展,Modbus已经成为工业自动化领域中最常用的通信协议之一,尤其适用于连接工业电子设备。本文将详细…...
鸿蒙笔记--装饰器
这一节主要了解一下鸿蒙里的装饰器,装饰器是一种特殊的语法结构,用于装饰类、结构体、方法以及变量; 1 Component在鸿蒙(HarmonyOS)开发中扮演着重要角色,主要用于定义可重用的UI组件,主要作用:1)组件化:Component装饰…...

【反无人机检测】C2FDrone:基于视觉Transformer网络的无人机间由粗到细检测
C2FDrone:基于视觉Transformer网络的无人机间由粗到细检测 C2FDrone: Coarse-to-Fine Drone-to-Drone Detection using Vision Transformer Networks 论文链接 摘要 摘要——基于视觉的无人机间检测系统在碰撞规避、反制敌对无人机和搜救行动等应用中至关重要。然…...

从理论崩塌到新路径:捷克科学院APL Photonics论文重构涡旋光技术边界
理论预言 vs 实验挑战 光子轨道角动量(Orbital Angular Momentum, OAM)作为光场调控的新维度,曾被理论预言可突破传统拉曼散射的对称性限制——尤其是通过涡旋光(如拉盖尔高斯光束)激发晶体中常规手段无法探测的"…...

DAY43打卡
浙大疏锦行 kaggle找到一个图像数据集,用cnn网络进行训练并且用grad-cam做可视化 进阶:并拆分成多个文件 fruit_cnn_project/ ├─ data/ # 存放数据集(需手动创建,后续放入图片) │ ├─ train/ …...

ComfyUI 对图片进行放大的不同方法
本篇里 ComfyUI Wiki将讲解 ComfyUI 中几种基础的放大图片的办法,我们时常会因为设备性能问题,不能一次性生成大尺寸的图片,通常会先生成小尺寸的图像然后再进行放大。 不同的放大图片方法有不同的特点,以下是本篇教程将会涉及的方法: 像素重新采样SD 二次采样放大使用放…...
SQL进阶之旅 Day 19:统计信息与优化器提示
【SQL进阶之旅 Day 19】统计信息与优化器提示 文章简述 在数据库性能调优中,统计信息和优化器提示是两个至关重要的工具。统计信息帮助数据库优化器评估查询成本并选择最佳执行计划,而优化器提示则允许开发人员对优化器的行为进行微调。本文深入探讨了…...
java教程笔记(十一)-泛型
Java 泛型(Generics)是 Java 5 引入的重要特性之一,它允许在定义类、接口和方法时使用类型参数。泛型的核心思想是将类型由具体的数据类型推迟到使用时再确定,从而提升代码的复用性和类型安全性。 1.泛型的基本概念 1. 什么是泛…...

当丰收季遇上超导磁测量:粮食产业的科技新征程
麦浪藏光阴,心田种丰年!又到了一年中最令人心潮澎湃的粮食丰收季。金色的麦浪随风翻滚,沉甸甸的稻穗谦逊地低垂着,处处洋溢着丰收的喜悦。粮食产业,无疑是国家发展的根基与命脉,是民生稳定的压舱石。在现代…...
Vue.js 组件:深入理解与实践
Vue.js 组件:深入理解与实践 引言 随着前端技术的不断发展,Vue.js 作为一种流行的前端框架,因其简洁、易学、高效的特点受到越来越多开发者的青睐。在Vue.js中,组件是构建用户界面的基石。本文将深入探讨Vue.js组件的概念、特性、创建方式以及在实际开发中的应用,帮助读…...

Django CMS 的 Demo
以下是关于 Django CMS 的 Demo 示例及相关资源的整理 安装与运行 Django CMS 示例 使用 djangocms-installer 快速创建 Django CMS 项目: pip install django_cms djangocms -p . mysite安装记录 pip install django-cms Looking in indexes: https://pypi.tun…...
Android协程学习
目录 Android上的Kotlin协程介绍基本概念与简单使用示例协程的高级用法 结构化并发线程调度器(Dispatchers)自定义调度器并发:同步 vs 异步 异步并发(async 并行执行)同步顺序执行协程取消与超时 取消机制超时控制异步数据流 Flow协程间通信 使用 Channel使用 StateFlow /…...