【数据结构】双向链表(真正的零基础)

链表是一种物理存储单元上非连续、非顺序的存储结构。数据元素的逻辑顺序是通过指针的链接来实现的!在上篇我们学习了单向链表,而单向链表虽然空间利用率高,插入和删除也只需改变指针就可以达到!但是我们在每次查找、删除、访问.....时,都受到单向链表的一个特性干扰:单向不可逆,只能以遍历的形式去使用,这样使得需要从头到尾找到特定的节点再进行操作!今天我们来学习另外一种链表结构:双向链表。既然是零基础,我会从小白的角度进行讲解!放心食用!
目录
一.链表的分类
二.带头双向循环链表
二.(1)链表节点设置
二.(2)链表新增节点
二.(3)链表初始化
二.(4)链表扩容节点
二.(5)链表打印
二.(6)在目标节点前面插入
二.(7)在目标节点后面插入
二.(8)链表头删
二.(9)链表尾删
二.(10)链表头插
二.(11)链表尾插
二.(12)删除中间目标节点
二.(13)释放双链表
三.总结
四.代码合并
一.链表的分类
这里就不介绍哈是链表了,如果对此感到疑惑,可以看我上一篇投稿,咱直接进入分类!
链表主要可以分为以下3大类:
单向链表:
单向链表是链表中最简单的一种,它由一系列节点组成,每个节点包含两个部分:指针域与数据域。指针域用来指向下一个节点的地址,数据域用来存储数据。以下是抽象草图,方便巧记:

双向链表:
双向链表与单向链表不同,它的每个节点有两个指针(next跟prev)跟一个数据域,两个指针分别指向前一个节点和后一个节点。同样,如图巧记:

循环链表:
循环链表是一种特殊的单链表,它的最后一个节点的指针指向头节点,形成一个“环”,如图:

在进行下面的学习之前,针对链表的各种分类,我们需要理清几个名词:
头指针:
头指针本质上是结构体类型的指针,指向一个节点,这个节点一般是 第一个节点 或者 头节点
头结点:
简单理解为在 第一个节点 前面还有一个节点,这个节点就称为头节点。头节点一般不存储数据
带头:
头指针指向头节点就称为带头节点,如图:

不带头:
头指针指向第一个节点就称为不带头节点,如图:

针对这几种分类:带头或者不带头、单向或者双向、循环或者非循环
我们总共可以组合八种链表,我们主要学习 无头双向非循环链表 带头双向循环链表 这两种!
八种链表结构中其中这两种最具代表性,另外六种便自然理解了!上一篇我们已经学完了第一种,今天我们来学习第二种!
注意:双链表一般需要改变四个指针的指向,因此建议大家画图理解!这样就可以轻松操作了!
二.带头双向循环链表
理解了上面几个名词后,我们就能轻易理解哈是带头双向循环链表了!
带头:无非就是第一个节点前还有一个头节点嘛!
双向:节点里面有两个指针,两个指针分别指向一前一后的节点
循环:尾节点的后指针指向头节点,头节点的前指针指向尾节点,达到封闭,形成循环!

二.(1)链表节点设置
不论怎样,我们都需要先创建一个结构体作为接下来各种功能的节点基础, 这个结构体里面有两个指针,一个数据域。这里为了缩短结构体类型,我们用 typedef 重定义以下,需要注意的是typedef重定义的这个结构体里面,结构体指针必须写完整,因为此处还属于声明阶段,后续才可以正常缩写。代码如下:
typedef struct Pointdef
{struct Pointdef* next;//指向下一个节点struct Pointdef* prev;//指向上一个节点int data;//数据域
}Pointdef;
二.(2)链表新增节点
因为我们不管初始化还是各种增添功能,都需要节点,所以我们先完成开辟节点的函数,让这个函数返回指向这个开辟好的节点指针,然后才可以进行初始化操作,代码如下:
//开辟新节点
Pointdef* Newspoint(int x)
{Pointdef* news = (Pointdef*)malloc(sizeof(Pointdef));if (news == NULL){perror("malloc");return NULL;}//初始化新节点news->next = NULL;news->prev = NULL;news->data = x;return news;
}
我们刚开辟好的节点还没有连接,只是一个结构体大小,因此它的prev跟next指针都指向NULL,x就是初始化节点的数据域,如下图理解:

二.(3)链表初始化
上面我们先调用 新增节点 的函数,在外面开辟一个节点作为头节点(如果有问题,可以先看下面的合并代码!),接下来对这个头节点进行初始化,将它作为头节点,此时头节点的next跟prev指针都应该指向自己(因为是循环链表,此时只有一个节点)。代码如下:
//链表初始化
void Initialize(Pointdef* head)
{head->next = head;head->prev = head;
}

二.(4)链表扩容节点
此时我们已经初始化了链表,下面我们给它新增几个节点,调用新增函数,改变节点指针指向,代码如下:
//链表扩容节点
Pointdef* tail = head;
for (int i = 2; i <= 5; i++)
{Pointdef* news = Newspoint(i);//连接tail->next = news;news->prev = tail;news->next = head;head->prev = news;tail = tail->next;
}

二.(5)链表打印
与单向链表访问区别不大,只需要注意是这里的链表是循环的,需要控制打印的条件不能是当前指针不能指向头节点,否则就是死循环了,代码如下:
//链表打印
void Printf(Pointdef* head)
{Pointdef* tail = head->next;while (tail != head){printf("%d <= ", tail->data);tail = tail->next;}printf("NULL\n");
}

二.(6)在目标节点前面插入
我们已经写好了一条双链表,那么我们只需要找到目标节点然后连接四个指针就行了,代码如下:
这里我们假设目标节点是 tail ,新节点 news 新节点前面的节点是 prev
//在目标节点前面插入
void FrontInsertion(Pointdef* tail ,int x)
{assert(tail);Pointdef* prev = tail->prev;Pointdef* news = Newspoint(x);prev->next = news;news->prev = prev;news->next = tail;tail->prev = news;}

二.(7)在目标节点后面插入
将找到的目标节点作为参数,然后在函数中改变它的前后四个指针就行,如图:
//在目标节点后面插入
void OutInsertion(Pointdef* tail, int x)
{assert(tail);//目标节点的下一个节点不能是空,否则就是尾插了assert(tail->next);Pointdef* news = Newspoint(x);Pointdef* next = tail->next;tail->next = news;news->prev = tail;news->next = next;next->prev = news;
}

二.(8)链表头删
头删咱直接将头节点作为参数就行了,再记录第二个节点方便释放,然后用四个指针连接头节点和第二个节点,最后记得释放第一个节点的空间,如图:
//链表头删
void Outomit(Pointdef* head,int x)
{assert(head);assert(head->next);Pointdef* tail = head->next->next;Pointdef* next = head->next;head->next = tail;tail->prev = head;free(next);next = NULL;
}

二.(9)链表尾删
这个咱就不多说了!就是先记录倒数第二个节点,将它作为尾节点就行了,同样记得释放空间,直接上代码:
//链表尾删
void Tailomit(Pointdef* head, int x)
{assert(head);Pointdef* prev = head->prev->prev;Pointdef* tail = head->prev;prev->next = head;head->prev = prev;free(tail);tail = NULL;
}

二.(10)链表头插
但凡插入节点的都具有很大的相似之处,无非就是将这个节点插入某处,通过记录附近节点的位置,再把新增节点的左右节点与它进行连接,看代码:
//链表头插
void Headinsert(Pointdef* head, int x)
{assert(head);Pointdef* tail = head->next;Pointdef* news = Newspoint(x);head->next = news;news->prev = head;news->next = tail;tail->prev = news;
}

二.(11)链表尾插
//链表尾插
void Tailinsert(Pointdef* head, int x)
{assert(head);Pointdef* tail = head->prev;Pointdef* news = Newspoint(x);tail->next = news;news->prev = tail;head->prev = news;news->next = head;
}

二.(12)删除中间目标节点
删除节点我们肯定要先找到目标节点,我们在链表中可以通过以下两种方式寻找:指针域 数据域,在上面的前后插入中,我们选择的指针查找方式,下面我们进行数据查找方式来删除:
//链表中间节点删除
void Omit(Pointdef* tail)
{Pointdef* pc = tail->prev;Pointdef* pt = tail->next;pc->next = pt;pt->prev = pc;free(tail);tail = NULL;
}

二.(13)释放双链表
咋从第一个节点释放到尾就OK了!注意:链表的空间不是连续的,因此需要一个个销毁!
//双链表销毁
void Undermine(Pointdef* head)
{assert(head);Pointdef* cur = head->next;Pointdef* tail = cur->next;while (cur != head){tail = cur->next;free(cur);cur = tail;}free(head);head=NULL;printf("释放完毕\n");
}
三.总结
针对以上实现的双链表,其实针对整个链表,都有以下特点:
1:首先我们要有设计好开辟节点的函数,然后知道如何连接这些节点
2:头插、尾插、中间插其实都有很大的共性,只是目标地点稍微差异,这点大家可以画图理解!
3:删除某个节点需要及时释放该空间,避免空间泄漏。需要注意是先连接,再释放
4:针对为何单向链表很多涉及二级指针,而双向链表大多是一级指针?
因为单向链表有很多地方需要改变头指针(单向链表将第一个节点作为头节点)的指向,而双向链表是固定了哨兵节点,因此其它操作其实是针对结构体里面的指针操作的。该知识的原理是:对形参的改变不影响实参,如需改变形参,需要使用二级指针
5:双向链表为哈要从第一个节点开始释放?
双向链表的头节点是链表结构的一部分,不是数据节点,应该先释放数据节点,再释放链表结构
四.代码合并
流程文件(可以使用打印来进行功能的测试!)
#define _CRT_SECURE_NO_WARNINGS 1
#include"text.h"int main()
{int x = 4;//链表新增节点Pointdef* head=Newspoint(1);//链表初始化void Initialize(head);//链表扩容节点Pointdef* tail = head;for (int i = 2; i <= 5; i++){Pointdef* news = Newspoint(i);//连接tail->next = news;news->prev = tail;news->next = head;head->prev = news;tail = tail->next;}//链表打印Printf_t(head);//在目标节点前面插入 tail = (head->next)->next;x = 6;FrontInsertion(tail, x);//在目标节点后面插入 OutInsertion(tail, x);// 测试点Printf_t(head);//链表头删x = 7;Outomit(head,x);//链表尾删x = 8;Tailomit(head, x);// 测试点Printf_t(head);//链表头插x = 1;Headinsert(head, x);//链表尾插x = 9;Tailinsert(head, x);// 测试点Printf_t(head);//链表中间节点删除x = 4;tail = head -> next;while (tail->data != x){tail = tail->next;}Omit(tail);// 测试点Printf_t(head);//双链表销毁Undermine(head);return 0;
}
头文件
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdlib.h>
#include<stdio.h>
#include<assert.h>typedef struct Pointdef
{struct Pointdef* next;//指向下一个节点struct Pointdef* prev;//指向上一个节点int data;//数据域
}Pointdef;//链表新增节点
Pointdef* Newspoint(int x);
//链表初始化
void Initialize(Pointdef* head);
//链表打印
void Printf_t(Pointdef* head);
//在目标节点前面插入
void FrontInsertion(Pointdef* tail, int x);
//在目标节点后面插入
void OutInsertion(Pointdef* tail, int x);
//链表头删
void Outomit(Pointdef* head ,int x);
//链表尾删
void Tailomit(Pointdef* head, int x);
//链表头插
void Headinsert(Pointdef* head, int x);
//链表尾插
void Tailinsert(Pointdef* head, int x);
//链表中间节点删除
void Omit(Pointdef* tail);
//双链表销毁
void Undermine(Pointdef* head);
函数执行文件
#define _CRT_SECURE_NO_WARNINGS 1
#include"text.h"//链表新增节点
Pointdef* Newspoint(int x)
{Pointdef* news = (Pointdef*)malloc(sizeof(Pointdef));if (news == NULL){perror("malloc");return NULL;}//初始化新节点news->next = NULL;news->prev = NULL;news->data = x;return news;
}//链表初始化
void Initialize(Pointdef* head)
{head->next = head;head->prev = head;
}//链表打印
void Printf_t(Pointdef* head)
{Pointdef* tail = head->next;while (tail != head){printf("%d <= ", tail->data);tail = tail->next;}printf("NULL\n");
}//在目标节点前面插入
void FrontInsertion(Pointdef* tail ,int x)
{assert(tail);Pointdef* prev = tail->prev;Pointdef* news = Newspoint(x);prev->next = news;news->prev = prev;news->next = tail;tail->prev = news;}//在目标节点后面插入
void OutInsertion(Pointdef* tail, int x)
{assert(tail);//目标节点的下一个节点不能是空,否则就是尾插了assert(tail->next);Pointdef* news = Newspoint(x);Pointdef* next = tail->next;tail->next = news;news->prev = tail;news->next = next;next->prev = news;
}//链表头删
void Outomit(Pointdef* head,int x)
{assert(head);assert(head->next);Pointdef* tail = head->next->next;Pointdef* next = head->next;head->next = tail;tail->prev = head;free(next);next = NULL;
}//链表尾删
void Tailomit(Pointdef* head, int x)
{assert(head);Pointdef* prev = head->prev->prev;Pointdef* tail = head->prev;prev->next = head;head->prev = prev;free(tail);tail = NULL;
}//链表头插
void Headinsert(Pointdef* head, int x)
{assert(head);Pointdef* tail = head->next;Pointdef* news = Newspoint(x);head->next = news;news->prev = head;news->next = tail;tail->prev = news;
}//链表尾插
void Tailinsert(Pointdef* head, int x)
{assert(head);Pointdef* tail = head->prev;Pointdef* news = Newspoint(x);tail->next = news;news->prev = tail;head->prev = news;news->next = head;
}//链表中间节点删除
void Omit(Pointdef* tail)
{Pointdef* pc = tail->prev;Pointdef* pt = tail->next;pc->next = pt;pt->prev = pc;free(tail);tail = NULL;
}//双链表销毁
void Undermine(Pointdef* head)
{assert(head);Pointdef* cur = head->next;Pointdef* tail = cur->next;while (cur != head){tail = cur->next;free(cur);cur = tail;}free(head);head = NULL;printf("释放完毕\n");
}
相关文章:
【数据结构】双向链表(真正的零基础)
链表是一种物理存储单元上非连续、非顺序的存储结构。数据元素的逻辑顺序是通过指针的链接来实现的!在上篇我们学习了单向链表,而单向链表虽然空间利用率高,插入和删除也只需改变指针就可以达到!但是我们在每次查找、删除、访问..…...
【生产变更】- Oracle RAC添加配置ipv6地址
【生产变更】- Oracle RAC添加配置ipv6地址 一、概述二、环境检查及备份2.1 检查并备份系统层面IP配置2.2 检查并备份监听配置2.3 检查并备份网卡配置2.4 检查并备份/etc/hosts三、集群层面配置3.1 检查集群配置3.2 停止集群组件3.3 Bond0网卡设置3.4 /etc/hosts文件配置3.5 重…...
Ai无限免费生成高质量ppt教程(deepseek+kimi)
第一步:打开deepseek官网(DeepSeek) 1.如果deepseek官网网络繁忙,解决方案如下: (1)使用easychat官网(EasyChat)使用deepseek模型,如图所示: (2)本地部署&…...
python全栈-python基础
python基础 文章目录 python基础python入门基础概念序列列表元组 -- 不可变序列字典字典的本质集合 控制语句选择结构 - 条件判断结构循环结构zip()推导式 函数及原理参数LEGB规则 面向对象私有属性和私有方法面向对象的特征重写__str__()方法super获得父类的定义特殊方法和运算…...
Python 鼠标轨迹 - 防止游戏检测
一.简介 鼠标轨迹算法是一种模拟人类鼠标操作的程序,它能够模拟出自然而真实的鼠标移动路径。 鼠标轨迹算法的底层实现采用C/C语言,原因在于C/C提供了高性能的执行能力和直接访问操作系统底层资源的能力。 鼠标轨迹算法具有以下优势: 模拟…...
力扣 零钱兑换
完全背包,动态规划例题。 题目 这题跟完全背包跟完全平方数有点相似。在完全平方数中,用一个dp数组去取得目标金额的每一步的最优,当前状态可能来自上一个dp,也有可能比上一个dp更小,因此往回退一步加一做比较。在完全…...
C# OpenCV机器视觉:OSTU算法实现背景差分的自适应分割
在一个热闹的科技公司里,阿强是一个负责图像分析的员工。他的日常工作就是从各种复杂的图像中提取出有用的信息,可这可不是一件轻松的事情哦 最近,阿强接到了一个艰巨的任务:要从一堆嘈杂的监控图像中分离出运动的物体,…...
快速搭建 Elasticsearch 8 集群:零基础实战与升级注意事项
引言 随着大数据技术的飞速发展,Elasticsearch 成为许多应用场景中不可或缺的技术,它以其高效的全文搜索引擎和分布式存储架构在企业和个人项目中占据了一席之地。无论是在日志分析、实时搜索还是数据可视化中,Elasticsearch 都发挥着重要的作用。 在这篇文章中,我们将为…...
基于扑克牌分发效果制作时的问题总结
其基本效果如图 1. 在overlay模式下直接使用position来移动 实现代码 public class Card : MonoBehaviour {public RectTransform target;public Button cardButton;private bool isPack false;public List<RectTransform> cards new List<RectTransform>(…...
老榕树的Java专题:Redis 从入门到实践
一、引言 在当今的软件开发领域,数据的高效存储和快速访问是至关重要的。Redis(Remote Dictionary Server)作为一个开源的、基于内存的数据结构存储系统,因其高性能、丰富的数据类型和广泛的应用场景,成为了众多开发者…...
【玩转 Postman 接口测试与开发2_019】第15章:利用 Postman 初探 API 性能测试(含实战截图)
《API Testing and Development with Postman》最新第二版封面 文章目录 第十五章 API 接口性能测试1 性能负载的类型2 Postman 负载配置3 Postman 性能测试实战3.1 Fixed 型负载下的性能测试3.2 基于数据驱动的 Postman 接口性能测试 4 性能测试的注意事项 写在前面 终于来到了…...
在 Qt 开发中,可以将 QML 封装成库
在 Qt 开发中,可以将 QML 封装成库,以便在多个项目中复用 QML 组件或模块。下面通过一个简单的例子说明如何将 QML 封装成库并在其他项目中使用。 1. 创建 QML 库项目 首先,我们创建一个新的 Qt 项目,专门用于封装 QML 组件。假…...
换电脑了如何快速导出vscode里的插件
当你换电脑了,之前vscode里的插件又不想全部手动重装,那么恭喜你,刷到了这篇文章。 1. 将 VSCode 添加到系统路径 macOS 打开 VSCode。按下 Command Shift P 打开命令面板。 3。 输入 Shell Command: Install ‘code’ command in PATH …...
点大商城V2-2.6.6源码全开源uniapp +搭建教程
一.介绍 点大商城V2独立开源版本,版本更新至2.6.6,系统支持多端,前端为UNiapp,多端编译。 二.搭建环境: 系统环境:CentOS、 运行环境:宝塔 Linux 网站环境:Nginx 1.21 MySQL 5.…...
9 Pydantic复杂数据结构的处理
在构建现代 Web 应用时,我们往往需要处理复杂的输入和输出数据结构。例如,响应数据可能包含嵌套字典、列表、元组,甚至是多个嵌套对象。Pydantic 是一个强大的数据验证和序列化库,可以帮助我们轻松地处理这些复杂的数据结构&#…...
springboot+redis实现将树形结构存储到redis
1.pom配置redis <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>2.yml文件配置: spring:redis:database: 0host: 1.1.1.1port: 6379timeout:…...
6、使用one-api管理统一管理大模型,并开始使用本地大模型
文章目录 本节内容介绍集中接入:将大模型统一管理起来当使用了大模型代理大模型代理示例 开源模型:如何使用Hugging Face上的模型modelscope使用 pipeline 调用模型用底层实现调用模型流式输出 如何在项目中使用开源模型使用 LangChain使用集中接入开始使…...
Windows安装Lyx
Lyx介绍 LyX 是一个基于 LaTeX 的可视化编辑器,可以让在不编写 LaTeX 代码的情况下使用 LaTeX 的排版功能。 需要依赖Latex环境,如Tex live 或者 MiKTeX Lyx 官网 Lyx官网 安装包下载 点击download默认进入最新版本下载界面 在Recent News/ News里可选…...
一文讲透大模型部署工具ollama--结合本地化部署deepseek实战
Ollama 是一个开源的人工智能平台,专注于在本地高效运行大型语言模型(LLMs)。通过 Ollama,开发者可以在自己的机器上运行多种大规模语言模型,而不必依赖于云端服务。它支持对大模型的管理和本地化部署,并且…...
网络防御高级
接口配置: SW2: [sw2]vlan 10 [sw2]vlan 20 [sw2]interface GigabitEthernet 0/0/1 [sw2-GigabitEthernet0/0/1]port link-type trunk [SW2-GigabitEthernet0/0/1]port trunk allow-pass vlan 10 20 [sw2]interface GigabitEthernet 0/0/2 [sw2-GigabitEthernet0/0/…...
Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...
Module Federation 和 Native Federation 的比较
前言 Module Federation 是 Webpack 5 引入的微前端架构方案,允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...
Springboot社区养老保险系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,社区养老保险系统小程序被用户普遍使用,为方…...
HDFS分布式存储 zookeeper
hadoop介绍 狭义上hadoop是指apache的一款开源软件 用java语言实现开源框架,允许使用简单的变成模型跨计算机对大型集群进行分布式处理(1.海量的数据存储 2.海量数据的计算)Hadoop核心组件 hdfs(分布式文件存储系统)&a…...
Yii2项目自动向GitLab上报Bug
Yii2 项目自动上报Bug 原理 yii2在程序报错时, 会执行指定action, 通过重写ErrorAction, 实现Bug自动提交至GitLab的issue 步骤 配置SiteController中的actions方法 public function actions(){return [error > [class > app\helpers\web\ErrorAction,],];}重写Error…...
前端异步编程全场景解读
前端异步编程是现代Web开发的核心,它解决了浏览器单线程执行带来的UI阻塞问题。以下从多个维度进行深度解析: 一、异步编程的核心概念 JavaScript的执行环境是单线程的,这意味着在同一时间只能执行一个任务。为了不阻塞主线程,J…...
Async-profiler 内存采样机制解析:从原理到实现
引言 在 Java 性能调优的工具箱中,async-profiler 是一款备受青睐的低开销采样分析器。它不仅能分析 CPU 热点,还能精确追踪内存分配情况。本文将深入探讨 async-profiler 实现内存采样的多种机制,结合代码示例解析其工作原理。 为什么需要内…...
SeaweedFS S3 Spring Boot Starter
SeaweedFS S3 Spring Boot Starter 源码特性环境要求快速开始1. 添加依赖2. 配置文件3. 使用方式方式一:注入服务类方式二:使用工具类 API 文档SeaweedFsS3Service 主要方法SeaweedFsS3Util 工具类方法 配置参数运行测试构建项目注意事项集成应用更多项目…...
