当前位置: 首页 > news >正文

双向链表 -- 详细理解和实现

                                                                                         欢迎光顾我的homepage                                                              

前言

        双向链表是一种带头双向循环的链表。在双向链表中,首先存在着一个头结点;其次每个节点有指向下一个节点的指针next 和指向上一个节点的指针prev ;最后,双向链表的头结点中存放上一个节点的指针指向链表的尾节点,尾节点中存放下一个节点的指针指向链表的头结点,形成一个闭环。这样双向链表既可以从前遍历,也可以从后遍历,直到回到起点。

一、链表的分类

        链表的结构多种多样,链表呢可以是带头(不带头)、双向(单向)、循环(不循环)的,我们之前实现的单链表其实就是不带头,单向,不循环的链表。

而这些有什么区别呢?

        带头和不带头

这里带头和不带头指的是有没有头结点,这单链表的实现过程中,是直接创建一个指针变量指向链表的第一个节点(这是没有头结点的情况),而存在头结点呢,就是一个哨兵位,里面没有存放数据,存放了指向第一个节点的指针。

        可以看到,带头的链表多了一个节点(head),这个节点中没有存放任何数据,这样做可以方便对链表的节点进行统一操作。

        单向和双向

    单向是可以通过一个节点找到它的后一个节点,而双向是可以通过一个节点找到它前后的节点。

        循环和不循环

    这实现单链表的时候,我们将链表的最后一个节点next指针置位了空指针(NULL),而循环的链表中,我们会将最后一个节点的next指针指向链表的头结点,对于双向链表,将头节点的prev(上一个节点)指针指向链表的尾节点。

二、双向链表的实现

这里实现的双向链表,先来看一下双向链表的节点结构

双向链表节点

typedef int LTDataType;
//双向链表
typedef struct ListNode
{struct ListNode* prev;  //指向上一个节点struct ListNode* next;  //指向下一个节点LTDataType data;
}LTNode;

双向链表的功能预览

//初始化并创建头结点
void LTInit(LTNode** phead);
//LTNode* LTInit();
//输出
void LTPrint(LTNode* phead);
//创建新的节点
LTNode* LTBuyNode(LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//尾删
void LTPopBack(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType find);
//在pos节点之后插入数据
void LTPushAfter(LTNode* pos, LTDataType x);
//在pos节点之前插入数据
void LTPush(LTNode* pos, LTDataType x);
//删除pos节点
void LTPop(LTNode* pos);
//链表销毁
void LTDesTroy(LTNode* phead);

2.1、创建新的节点

        创建节点,和单链表一样,都是动态开辟的空间。

//创建新的节点
LTNode* LTBuyNode(LTDataType x)
{LTNode* ptail = (LTNode*)malloc(sizeof(LTNode));if (ptail == NULL){perror("LTBuyNode--malloc filed!");exit(1);}ptail->data = x;ptail->prev = ptail->next = NULL;return ptail;
}

2.2、双向链表初始化并创建头结点

        链表初始化,其实就是创建一个头节点(也叫哨兵位);因为这里是双向链表,创建完头节点之后,让头节点的prev和next指针都指向它自己。

//初始化并创建头结点
void LTInit(LTNode** phead)
{*phead = LTBuyNode(-1);(*phead)->prev = (*phead)->next = *phead;
}

2.3、输出链表

        遍历链表并输出有效数据,这里双向链表可以从前开始遍历也可以从后开始遍历。

//输出
void LTPrint(LTNode* phead)
{LTNode* ptail = phead->next;//遍历双向链表 -- 从前开始遍历while (ptail != phead){printf("%d -> ", ptail->data);ptail = ptail->next;}printf("\n");
}

2.4、双向链表头插数据

        从链表的头部插入数据,创建新的节点,然后将新的节点prev指针指向头节点,将next指针指向头节点的下一个节点;然后修改头节点的next指针和头节点下一个节点的prev指针即可。

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->prev = phead;newnode->next = phead->next;//修改前后节点的指针指向phead->next->prev = newnode;phead->next = newnode;
}

2.5、双向链表尾插数据

        从链表尾部插入到尾节点的后面,创建新的节点,将新节点的prev指针指向头节点的上一个节点,将next指针指向头节点,然后修改尾节点的next指针和头节点的prev指针即可。

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->prev = phead->prev;newnode->next = phead;//修改前后节点指针指向phead->prev->next = newnode;phead->prev = newnode;
}

2.6、双向链表头删数据

        头删是删除头节点的后一个节点,因为是动态开辟的内存,需要内存释放;删除后,让头节点的next指针 指向下一个数据的节点。

//头删
void LTPopFront(LTNode* phead)
{assert(phead && phead != phead->next); //链表为空的话就不能进行删除LTNode* del = phead->next;  phead->next->next->prev = phead;phead->next = phead->next->next;free(del);del = NULL;
}

2.7、双向链表尾删数据

        尾插是删除链表的尾节点,释放内存之后,让尾节点的上一个节点next指针指向头节点,头节点的prev指针指向删除节点的上一个节点。

//尾删
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;
}

2.8、双向链表查找数据

        这里如果找到了,就返回该节点的指针,如果没有就返回NULL;方便对其进行前后插入数据和删除。

//查找
LTNode* LTFind(LTNode* phead, LTDataType find)
{assert(phead);LTNode* ptail = phead->next;while (ptail != phead) //遍历链表{if (ptail->data == find) {return ptail;}ptail = ptail->next;}return NULL;
}

2.9、在指定节点pos之前插入数据

        根据我们查找到的节点,在其之前插入数据,首先创建新节点,将新节点的prev指针指向pos的前一个节点,新节点的next指针指向pos;再修改pos的上一个节点的next指针指向和pos的prev指针指向即可;

//在pos节点之前插入数据
void LTPush(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos;newnode->prev = pos->prev;//修改pos前后节点指针的指向pos->prev->next = newnode;pos->prev = newnode;
}

        这里就可以发现双向链表的一个好处,不用像单向链表那样遍历链表来寻找pos的上一个节点。

2.10、在指定节点pos之后插入数据

        根据查找到的节点,在其之后插入数据;首先创建节点,将新节点的prev指针指向pos,新节点的next指针指向pos的下一个节点;然后修改pos的next指针指向和pos下一个节点的prev指针指向即可。

//在pos节点之后插入数据
void LTPushAfter(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;newnode->prev = pos;//修改pos前后节点指针的指向pos->next->prev = newnode;pos->next = newnode;
}

2.11、删除指定节点pos

        根据查找到的节点,然后将其删除,这里需要修改pos前后节点的指针指向。

//删除pos节点
void LTPop(LTNode* pos)
{assert(pos);pos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}

2.12、双向链表的销毁

        及时对动态开辟的内存进行释放,养成好习惯

        现在,动态开辟的链表需要进行销毁(也就是动态内存释放),这里就需要遍历链表了。

//链表销毁
void LTDesTroy(LTNode* phead)
{assert(phead);LTNode* ptail = phead->next;while (ptail != phead){LTNode* del = ptail->next;free(ptail);ptail = del;}free(ptail);ptail = NULL;
}


代码总览

List.h

#define _CRT_SECURE_NO_WARNINGS#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int LTDataType;
//双向链表
typedef struct ListNode
{struct ListNode* prev;  //指向上一个节点struct ListNode* next;  //指向下一个节点LTDataType data;
}LTNode;//初始化并创建头结点
void LTInit(LTNode** phead);
//LTNode* LTInit();
//输出
void LTPrint(LTNode* phead);
//创建新的节点
LTNode* LTBuyNode(LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//尾删
void LTPopBack(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType find);
//在pos节点之后插入数据
void LTPushAfter(LTNode* pos, LTDataType x);
//在pos节点之前插入数据
void LTPush(LTNode* pos, LTDataType x);
//删除pos节点
void LTPop(LTNode* pos);
//链表销毁
void LTDesTroy(LTNode* phead);

List.c

#include"List.h"//创建新的节点
LTNode* LTBuyNode(LTDataType x)
{LTNode* ptail = (LTNode*)malloc(sizeof(LTNode));if (ptail == NULL){perror("malloc filed!");exit(1);}ptail->data = x;ptail->prev = ptail->next = NULL;return ptail;
}
//初始化并创建头结点
void LTInit(LTNode** phead)
{*phead = LTBuyNode(-1);(*phead)->prev = (*phead)->next = *phead;
}
//输出
void LTPrint(LTNode* phead)
{LTNode* ptail = phead->next;//遍历双向链表 -- 从前开始遍历while (ptail != phead){printf("%d -> ", ptail->data);ptail = ptail->next;}printf("\n");
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->prev = phead;newnode->next = phead->next;//修改前后节点的指针指向phead->next->prev = newnode;phead->next = newnode;
}
//尾插
void LTPushBack(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 LTPopFront(LTNode* phead)
{assert(phead && phead != phead->next); //链表为空的话就不能进行删除LTNode* del = phead->next;  phead->next->next->prev = phead;phead->next = phead->next->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 find)
{assert(phead);LTNode* ptail = phead->next;while (ptail != phead) //遍历链表{if (ptail->data == find) {return ptail;}ptail = ptail->next;}return NULL;
}
//在pos节点之前插入数据
void LTPush(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos;newnode->prev = pos->prev;//修改pos前后节点指针的指向pos->prev->next = newnode;pos->prev = newnode;
}
//在pos节点之后插入数据
void LTPushAfter(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;newnode->prev = pos;//修改pos前后节点指针的指向pos->next->prev = newnode;pos->next = newnode;
}

感谢各位大佬支持并指出问题,

                如果本篇内容对你有帮助,可以一键三连支持以下,感谢支持!!!

相关文章:

双向链表 -- 详细理解和实现

欢迎光顾我的homepage 前言 双向链表是一种带头双向循环的链表。在双向链表中&#xff0c;首先存在着一个头结点&#xff1b;其次每个节点有指向下一个节点的指针next 和指向上一个节点的指针prev &#xff1b…...

WebGIS面试题

文章目录 1. 前端1.1. 选择器的优先级1.2. CSS 中它的布局有哪些&#xff1f;1.3. CSS3 的新特性1.4. CSS 的两种盒子模型1.5. CSS 的伪元素选择器和伪类选择器有哪些&#xff1f;1.6. ES6 的新特性1.7. 谈谈你对 promise 的理解1.8. 简单说一下原型链1.9. 简单说一下深浅拷贝1…...

代码随想录算法训练营:21/60

非科班学习算法day21 | LeetCode669:修剪二叉搜索树 &#xff0c;Leetcode108:将有序数组转换为二叉搜索树 &#xff0c;Leetcode538:把二叉搜索树转换为累加树 介绍 包含LC的两道题目&#xff0c;还有相应概念的补充。 相关图解和更多版本&#xff1a; 代码随想录 (progra…...

数据结构——二叉树之c语言实现堆与堆排序

目录 前言&#xff1a; 1.二叉树的概念及结构 1.1 特殊的二叉树 1.2 二叉树的存储结构 1.顺序存储 2.链式存储 2. 二叉树的顺序结构及实现 2.1 堆的概念 ​编辑 2.2 堆的创建 3.堆的实现 3.1 堆的初始化和销毁 初始化&#xff1a; 销毁&#xff1a; 插入&…...

#数据结构 链表

单向链表 1. 概念 单向链表 单向循环链表 双向链表 双向循环链表 解决&#xff1a;长度固定的问题&#xff0c;插入和删除麻烦的问题 1、逻辑结构&#xff1a; 线性结构 2、存储结构&#xff1a; 链式存储 链表就是将 结点 用链串起来的线性表&#xff0c;链就是 结点 中的…...

单片机软件架构连载(4)-结构体

枚举、指针、结构体&#xff0c;我愿称为C语言"三板斧"。 用人话来讲&#xff0c;几乎所有c语言高阶编程&#xff0c;都离不开这这3个知识点的应用。 今天站在实际产品常用的角度&#xff0c;给大家讲一下结构体。 1.结构体概念 结构体可以用来构建更复杂的数据结…...

工厂方法模式在金融业务中的应用及其框架实现

引言 工厂方法模式&#xff08;Factory Method Pattern&#xff09;是一种创建型设计模式&#xff0c;它定义了一个创建对象的接口&#xff0c;但由子类决定实例化哪一个类。工厂方法模式使得类的实例化延迟到子类。在金融业务中&#xff0c;工厂方法模式可以用于创建不同类型…...

python库(6):Pygments库

1 Pygments介绍 在软件开发和文档编写中&#xff0c;代码的可读性是至关重要的一环。无论是在博客文章、技术文档还是教程中&#xff0c;通过代码高亮可以使程序代码更加清晰和易于理解。而在Python世界中&#xff0c;Pygments库就是这样一个强大的工具&#xff0c;它能够将各…...

金斗云 HKMP智慧商业软件 任意用户创建漏洞复现

0x01 产品简介 金斗云智慧商业软件是一款功能强大、易于使用的智慧管理系统,通过智能化的管理工具,帮助企业实现高效经营、优化流程、降低成本,并提升客户体验。无论是珠宝门店、4S店还是其他零售、服务行业,金斗云都能提供量身定制的解决方案,助力企业实现数字化转型和智…...

前端JS特效第24集:jquery css3实现瀑布流照片墙特效

jquery css3实现瀑布流照片墙特效&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下(全部代码在文章末尾)&#xff1a; <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8" /> <title>jquerycss3实现瀑…...

区块链论文速读A会-ISSTA 2023(2/2)如何检测DeFi协议中的价格操纵漏洞

Conference&#xff1a;ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA) CCF level&#xff1a;CCF A Categories&#xff1a;Software Engineering/System Software/Programming Languages Year&#xff1a;2023 第1~5篇区块链文章 请点击此…...

权力之望怎么下载客户端 权力之望一键下载

《权力之望》是一款由NX3 Games开发、Smilegate发行的多人在线动作MMORPG游戏。这款游戏最大的特点是高度的自由度和丰富的角色定制选项。我们在游戏中不仅可以自由更换武器&#xff0c;而且游戏还提供了54种能力和60多种职业选择&#xff0c;让我们可以根据自己的游戏风格和喜…...

Oracle PL/SQL 循环批量执行存储过程

1. 查询存储过程 根据数据字典USER_OBJECTS查询出所有存储过程。 2. 动态拼接字符串&#xff08;参数等&#xff09; 根据数据字典USER_ARGUMENTS动态拼接参数。 3. 动态执行 利用EXECUTE IMMEDIATE动态执行无名块。 4. 输出执行信息 利用DBMS_OUTPUT.PUT_LINE输出执行成功与…...

kafka 生产者

生产者 生产者负责创建消息&#xff0c;然后将其投递到Kafka中。 负载均衡 轮询策略。随机策略。按照 key 进行hash。 Kafka 的默认分区策略&#xff1a;如果指定了 key&#xff0c;key 相同的消息会发送到同一个分区&#xff08;分区有序&#xff09;&#xff1b;如果没有…...

Powershell 获取电脑保存的所有wifi密码

一. 知识点 netsh wlan show profiles 用于显示计算机上已保存的无线网络配置文件 Measure-Object 用于统计数量 [PSCustomObject]{ } 用于创建Powershell对象 [math]::Round 四舍五入 Write-Progress 显示进度条 二. 代码 只能获取中文Windows操作系统的wifi密码如果想获取…...

golang结合neo4j实现权限功能设计

neo4j 是非关系型数据库之图形数据库&#xff0c;这里不再赘述。 传统关系数据库基于rbac实现权限, user ---- role ------permission,加上中间表共5张表。 如果再添上部门的概念&#xff1a;用户属于部门&#xff0c;部门拥有 角色&#xff0c;则又多了一层&#xff1a; user-…...

java 参数传递(尤其注意参数是对象的情况)

8大基本数据类型为 值传递 类和数组为 引用传递&#xff0c;传递的是地址 但是要注意虽然类是引用传递&#xff0c;但是要注意&#xff0c;调用方法是新开一个栈 因此如果进行p null或者 Person p new Person()等语句&#xff0c;要格外注意&#xff1a; 如果主函数再次输出…...

拼音字符串相似度

拼音字符串相似度 拼音字符串相似度介绍参考代码**编辑距离****余弦相似度****Jaccard相似度**参考文档拼音字符串相似度 介绍 拼音相似度是指在拼音转换后,两个拼音字符串之间的相似程度。常用的拼音相似度度量方法包括编辑距离、余弦相似度和 Jaccard 相似度等。 编辑距离…...

如何创建一个基本的Mojolicious Web应用:探索Perl的现代Web框架

如何创建一个基本的Mojolicious Web应用&#xff1a;探索Perl的现代Web框架 Mojolicious是一个用Perl编写的简单、优雅的Web开发框架&#xff0c;它提供了一套丰富的工具和方法&#xff0c;让开发者能够快速构建高性能的Web应用。本文将详细介绍如何创建一个基本的Mojolicious…...

FPGA/数字IC复习八股

一、FPGA概念&#xff0c;与数字IC的区别 二、FPGA底层逻辑 三、同步电路、异步电路以及优缺点 四、同步复位、异步复位、异步复位同步释放 深入理解复位---同步复位&#xff0c;异步复位&#xff0c;异步复位同步释放(含多时钟域&#xff09;_画出支持异步复位dff的电路图…...

K8s中pod的创建与销毁

刚开始学习&#xff0c;整了一下流程图1.pod的创建2.pod的销毁有不对的地方&#xff0c;大家共同探讨...

AI 视频生成美女跳舞测评 | 顶级 Prompt实测版(Grok Imagine、Kling AI 3.0、Veo 3.1)

兄弟们&#xff0c;AI 视频生成已经卷到飞起了&#xff01;之前写小黄文靠grok&#xff0c;现在生成“美女舞蹈”视频也得靠它。 今天上手实测截至今天热门的3款视频生成工具&#xff0c;专攻“美女跳舞”这个高难度场景&#xff1a;动作流畅度、人物一致性、性感画面感、提示…...

Phi-4-mini-reasoning推理模型5分钟快速上手:数学题逻辑题一键解答

Phi-4-mini-reasoning推理模型5分钟快速上手&#xff1a;数学题逻辑题一键解答 1. 为什么选择Phi-4-mini-reasoning&#xff1f; 如果你经常需要解决数学题、逻辑题或者需要一步步分析的问题&#xff0c;Phi-4-mini-reasoning就是为你量身定制的AI助手。这个模型不像那些通用…...

ESP8266天气时钟DIY全攻略:从零搭建到个性化定制

1. 硬件准备与成本控制 作为一个玩了多年智能硬件的爱好者&#xff0c;我强烈推荐从ESP8266开始入门物联网项目。这款芯片的价格实在太香了&#xff0c;9块钱就能买到NodeMCU开发板&#xff0c;性能却足够应付大多数DIY场景。我去年做过统计&#xff0c;用ESP8266搭建的天气时钟…...

clusterProfiler进阶指南:如何利用R语言进行多组学数据的功能富集分析与可视化

clusterProfiler进阶指南&#xff1a;如何利用R语言进行多组学数据的功能富集分析与可视化 在生物信息学领域&#xff0c;功能富集分析是将高通量组学数据转化为生物学洞见的关键步骤。作为R/Bioconductor生态中的明星工具&#xff0c;clusterProfiler以其强大的分析能力和丰富…...

国产铷原子钟 快稳铷原子钟突破铷钟启动时长痛点 铷钟 特种铷原子钟

在数字化浪潮席卷全球的今天&#xff0c;时频同步已成为支撑通信、电力、国防、科研等关键领域稳定运行的核心基石。从6G基站的纳秒级协同&#xff0c;到智能电网的故障精准定位&#xff0c;再到北斗导航的车道级精度保障&#xff0c;每一个场景都对时间频率的准确度、稳定度提…...

Flutter Documentation Website的布局系统:理解Flutter的约束模型

Flutter Documentation Website的布局系统&#xff1a;理解Flutter的约束模型 【免费下载链接】website Flutter documentation web site 项目地址: https://gitcode.com/gh_mirrors/websi/website Flutter Documentation Website的布局系统基于独特的约束模型&#xff…...

m4s-converter:释放B站缓存价值的格式转换利器

m4s-converter&#xff1a;释放B站缓存价值的格式转换利器 【免费下载链接】m4s-converter 一个跨平台小工具&#xff0c;将bilibili缓存的m4s格式音视频文件合并成mp4 项目地址: https://gitcode.com/gh_mirrors/m4/m4s-converter 价值对比&#xff1a;格式转换前后的效…...

VS2022 + WinForms:从拖控件到写逻辑,手把手带你做出第一个C#计算器

VS2022 WinForms&#xff1a;从拖控件到写逻辑&#xff0c;手把手带你做出第一个C#计算器 第一次打开Visual Studio 2022时&#xff0c;那个闪亮的启动界面可能会让你既兴奋又不知所措。作为微软最新的集成开发环境&#xff0c;VS2022为C#开发者提供了强大的工具链&#xff0…...

FreeSWITCH 1.10.10 图形化部署实战 - 麒麟V10 SP3 X86/ARM双架构服务器安装与配置指南

1. FreeSWITCH与麒麟V10 SP3的完美组合 FreeSWITCH作为一款开源的软交换平台&#xff0c;在企业通信、呼叫中心、即时通讯等领域有着广泛应用。而麒麟V10 SP3作为国产操作系统的代表&#xff0c;在信创领域扮演着重要角色。将这两者结合起来&#xff0c;既能满足国产化需求&am…...