【探索数据结构与算法】——深入了解双向链表(图文详解)
目录
一、双向链表的基本概念
二、双向链表的结构
三、双向链表的基本操作实现方法
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装饰…...
应对“中年危机”的前置策略:留学生入职第一天就该考虑的事情——如何建立你的“被动求职”网络?
在 2026 年的北美科技职场,拿到全职 Offer 签下字的那一刻,许多留学生会如释重负地认为自己终于进入了“保险箱”。然而,在残酷的宏观经济周期和快速迭代的 AI 浪潮面前,传统的“绝对稳定”早已不复存在。 无论是硅谷巨头…...
侧信道攻击防御指南:从智能家居到云服务器的7个关键防护措施
侧信道攻击防御指南:从智能家居到云服务器的7个关键防护措施 在数字化浪潮席卷全球的今天,数据安全已成为企业生存的命脉。然而,当大多数安全团队还在与传统的网络攻击周旋时,一种更为隐蔽的威胁正在悄然蔓延——侧信道攻击。这种…...
ReactiveObjC 核心概念解析:从 RACSignal 到 RACCommand
ReactiveObjC 核心概念解析:从 RACSignal 到 RACCommand 【免费下载链接】ReactiveObjC The 2.x ReactiveCocoa Objective-C API: Streams of values over time 项目地址: https://gitcode.com/gh_mirrors/re/ReactiveObjC ReactiveObjC 是一个强大的 Object…...
生化与分子生物学是搞不定导师的脑子的
偶然看到这个图,决定喷一下。为什么呢,因为我高三时候对生化与分子生物学非常感兴趣,我天生对精妙的结构架构比较痴迷,有研究和欲望。甚至考研都想考这个专业。我虽然不知道现在生化和分子生物领域硕博和导师们都在研究什么课题&a…...
嵌入式系统中联合体的高效数据管理实践
1. 联合体在嵌入式系统中的高效数据管理实践在嵌入式系统开发中,如何高效地管理和传输数据一直是个值得深入探讨的话题。最近我在一个智能家居控制项目中遇到了一个典型场景:需要同时管理7个用电器的开关状态和4组电源线参数(电压、电流、有功…...
郑州除甲醛好用源头厂家
新房装修后,甲醛问题成了无数家庭的“心头大患”。面对市场上琳琅满目的除甲醛产品和服务,消费者往往眼花缭乱,不知如何选择。今天,我们就来深入聊聊,在郑州,如何找到一个靠谱的除甲醛源头厂家,…...
HTML函数在ARM架构设备能运行吗_ARM硬件兼容性测试【详解】
HTML 本身没有函数,它不是编程语言;真正运行在 ARM 设备上的是 JavaScript、后端代码或 WebAssembly,主流浏览器和 Node.js 均原生支持 ARM 架构,问题多出在依赖的二进制模块或 wasm 文件架构不匹配。HTML函数?浏览器里…...
OpenClaw多模态研究助手:千问3.5-35B-A3B-FP8实现论文图表解析与笔记生成
OpenClaw多模态研究助手:千问3.5-35B-A3B-FP8实现论文图表解析与笔记生成 1. 为什么需要多模态研究助手 作为一名经常需要阅读前沿论文的研究者,我长期被两个问题困扰:一是PDF论文中的图表数据提取费时费力,二是阅读过程中的碎片…...
PyCharm Community 版新手一站式安装与配置指南
1. PyCharm Community版是什么? PyCharm Community版是JetBrains公司推出的免费Python集成开发环境(IDE),专为个人开发者和小型项目设计。我第一次接触这个工具时,发现它比想象中要强大得多 - 代码自动补全、错误检查、…...
Lychee-Rerank与MySQL协同实战:构建智能内容检索系统
Lychee-Rerank与MySQL协同实战:构建智能内容检索系统 你是不是也遇到过这样的烦恼?在自己的博客或者内容平台上,辛辛苦苦写的文章,用户却搜不到。明明文章里提到了某个技术点,但用户用关键词一搜,要么搜出…...

