DS:带头双向循环链表的实现(超详细!!)

创作不易,友友们给个三连吧!!!

      博主的上篇文章介绍了链表,以及单链表的实现。

单链表的实现(超详细!!)
    其实单链表的全称叫做不带头单向不循环链表,本文会重点介绍链表的分类以及双链表的实现!

一、链表的分类

   链表的结构⾮常多样,组合起来就有8种(2 x 2 x 2)链表结构:

1.1 单向或者双向

    双向链表,即上一个结点保存着下一个结点的地址,且下一个结点保存着上一个结点的地址,即我们可以从头结点开始遍历,也可以从尾结点开始遍历

1.2 带头或者不带头 

     单链表中我们提到的“头结点”的“头”和“带头”链表的头是两个概念!单链表中提到的“头结点”指的是第一个有效的结点,“带头”链表里的“头”指的是无效的结点(即不保存任何有效的数据!)

    

1.3 循环或者不循环

     不循环的链表最后一个结点的next指针指向NULL,而循环的链表,最后一个结点的next指针指向第一个结点!!

      虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 单链表(不带头单向不循环链表)和 双向链表(带头双向循环链表)

1. 无头单向非循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结 构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。

2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。

二、带头双向循环链表的结构

      带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨的”

“哨兵位”存在的意义:遍历循环链表避免死循环。

三、双向链表结点结构体的创建

     与单链表结点结构体不同的是,双向链表的结点结构体多了一个前驱结点!!

typedef int LTDataType;//对类型进行重命名,后面可以通过修改存储其他数据类型
typedef struct ListNode
{LTDataType data;//保存的数据struct ListNode* prev;//指针保存前一个结点的地址struct ListNode* next;//指针保存后一个结点的地址
}LTNode;

四、带头双向循环链表的实现

4.1 新结点的申请

      涉及到需要插入数据,都需要申请新节点,所以优先封装一个申请新结点的函数!利用返回值返回该结点

LTNode* LTBuyNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");exit(1);//申请失败需要强制退出程序}//申请成功,则新节点的前驱结点和后驱结点都指向自己newnode->data = x;newnode->prev = newnode->next = newnode;return newnode;
}

4.2 初始化(哨兵位结点)

       对于双向链表来说,需要优先创建一个哨兵结点,和其他结点不同的是,该哨兵结点可以不存储数据,这里我们默认给他一个-1。并利用返回值返回该结点。

LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);//哨兵结点可以不存储数据,我们默认给个-1return phead;//返回哨兵结点
}

4.3 尾插 

       如图,因为这个一个循环链表,相当于我们要把新节点插在最后一个结点和哨兵结点之间,并且最后一一个结点可以用哨兵结点的前驱结点(phead->prev)就可以找到,然后建立phead phead->prev newnode的联系!

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//申请新节点//建立phead phead->prev newnode的联系newnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;//尾结点的后继指针指向新节点phead->prev = newnode;//哨兵结点的前驱指针指向新结点
}

 单链表中我们的参数选择二级指针,为什么这里选择一级指针???

      对于单链表来收,单链表的头节点是会改变的,所以我们需要用二级指针,但是双链表的头节点相当于哨兵位,哨兵位是不需要被改变的,他是固定死的,所以我们选择了一级指针。(单链表改了完全头节点,但是双链表只会改变头结点的成员——prev和next)

注:phead->prev->next = newnode和phead->prev = newnode不能替换顺序,因为尾结点是通过头节点找到的,所以要优先让他与newnode建立联系,双链表虽然不需要像单链表一样找最后一个结点需要遍历链表,但是要十分注意修改指针指向的先后顺序!!

4.4 头插

       如图可知,相当于将新节点插入在头节点和头节点下一个结点之间,头节点下一个结点可以通过phead->next找到,然后建立phead、phead->next、newnode的联系!!

void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//申请新节点//建立phead phead->next newnode的联系newnode->prev = phead;newnode->next = phead->next;phead->next->prev = newnode;//头节点的下一个结点的前驱指针指向新结点phead->next = newnode;//头节点的后继指针指向新节点
}

4.5 打印

      因为是循环链表,所以为了避免死循环打印,我们要设置一个指针接收头节点的下一个结点,然后往后遍历,直到遍历到头节点结束。

void LTPrint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}

4.6 尾删

      由图可知,要建立phead和phead->prev->prev的联系,同时由于还要释放最后一个结点(phead->prev),所以在建立联系之前要现保存这个被释放的空间,等建立联系完再释放!!同时要注意一条规则,就是当链表中只有哨兵结点的时候,我们称该链表为空链表!因此如果链表只存在哨兵结点,那么删除是没有意义的,所以必须断言!

void LTPopBack(LTNode* phead)
{assert(phead);assert(phead->next != phead);//链表只有哨兵结点时删除没意义LTNode* del = phead->prev;//del记录最后一个结点del->prev->next = phead;//倒数第二个结点的后驱指针指向头结点phead->prev = del->prev;//头节点的前驱结点指向倒数第二个结点free(del);//释放最后一个结点del = NULL;
}

4.7 头删

       由图可知,要建立phead和phead->next->next的联系,同时由于还要释放第二个结点(phead->next),所以在建立联系之前要现保存这个被释放的空间,等建立联系完再释放!!

void LTPopFront(LTNode* phead)
{assert(phead);assert(phead->next != phead);//链表只有哨兵结点时删除没意义LTNode* del = phead->next;//del记录第二个结点del->next->prev = phead;//第二个结点的前驱指针指向头结点phead->next = del->next;//头节点的后驱指针指向第三个结点free(del);//释放第二个结点del = NULL;
}

4.8 查找

     涉及到对指定位置进行操作的时候,需要设置一个查找函数,根据我们需要的数据返回他的结点地址

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;
}

4.9 指定位置之后插入

        由图可知,指定位置插入相当于将新结点插入到指定位置(pos)和指定位置下一个结点的位置(pos->next),然后建立pos pos->next newnode的联系,而且这里用不到头节点!

void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);//保证pos为有效结点LTNode* newnode = LTBuyNode(x);//申请新节点//建立pos pos->next newnode的联系newnode->prev = pos;newnode->next = pos->next;pos->next->prev = newnode;//pos的后一个结点的前驱结点指向新节点pos->next = newnode;//pos结点的后继结点指向新结点
}

4.10 指定位置删除

       右图可知建立指定位置的前一个结点(pos->prev)和指定位置的后一个结点(pos->next)的联系,并释放pos。

void LTErase(LTNode* pos)
{assert(pos);//保证pos为有效结点pos->prev->next = pos->next;//pos的前一个结点的后继指针指向pos后一个结点pos->next->prev = pos->prev;//pos的后一个结点的前驱指针指向pos的前一个结点free(pos);//释放pospos = NULL;
}

4.11 销毁链表

void LTDestroy(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;LTNode*next = NULL;while (pcur != phead){next = pcur->next;free(pcur);pcur = next;}//除了头结点都释放完毕free(phead);//phead = NULL;//没有用!
}

为什么phead=NULL没有用??

       因为我们使用的是一级指针,这里相当于是值传递,值传递形参改变不了实参,所以将phead置空是没有意义的,其实如果这里使用二级指针,然后传地址就可以了,但是为了保持接口一致性,我们还是依照这种方法,但是phead=NULL必须在主函数中去使用,所以我们在调用销毁链表的函数的时候,别忘记了phead=NULL!!

五、带头双向循环链表实现的全部代码

List.h

#pragma once
#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* LTBuyNode(LTDataType x);//申请新的链表结点
LTNode* LTInit();//初始化(申请一个哨兵结点)
void LTPushBack(LTNode* phead, LTDataType x);//尾插 (最后一个结点后插入或哨兵结点前插入)
void LTPushFront(LTNode* phead, LTDataType x);//头插 (哨兵结点后的插入)
void LTPrint(LTNode* phead);//打印
void LTPopBack(LTNode* phead);//尾删
void LTPopFront(LTNode* phead);//头删
LTNode* LTFind(LTNode* phead, LTDataType x);//查找
void LTInsert(LTNode* pos, LTDataType x);//指定位置之后插入
void LTErase(LTNode* pos);//指定位置删除
void LTDestroy(LTNode* phead);//销毁链表

List.c

#include"List.h"LTNode* LTBuyNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");exit(1);//申请失败需要强制退出程序}//申请成功,则新节点的前驱结点和后驱结点都指向自己newnode->data = x;newnode->prev = newnode->next = newnode;return newnode;
}LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);//哨兵结点可以不存储数据,我们默认给个-1return phead;//返回哨兵结点
}void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//申请新节点//建立phead phead->prev newnode的联系newnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;//尾结点的后继结点指向新节点phead->prev = newnode;//哨兵结点的前驱指针指向新结点
}void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//申请新节点//建立phead phead->next newnode的联系newnode->prev = phead;newnode->next = phead->next;phead->next->prev = newnode;//头节点的下一个结点的前驱指针指向新结点phead->next = newnode;//头节点的后继指针指向新节点
}void LTPrint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}void LTPopBack(LTNode* phead)
{assert(phead);assert(phead->next != phead);//链表只有哨兵结点时删除没意义LTNode* del = phead->prev;//del记录最后一个结点del->prev->next = phead;//倒数第二个结点的后驱指针指向头结点phead->prev = del->prev;//头节点的前驱结点指向倒数第二个结点free(del);//释放最后一个结点del = NULL;
}void LTPopFront(LTNode* phead)
{assert(phead);assert(phead->next != phead);//链表只有哨兵结点时删除没意义LTNode* del = phead->next;//del记录第二个结点del->next->prev = phead;//第二个结点的前驱指针指向头结点phead->next = del->next;//头节点的后驱指针指向第三个结点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)
{assert(pos);//保证pos为有效结点LTNode* newnode = LTBuyNode(x);//申请新节点//建立pos pos->next newnode的联系newnode->prev = pos;newnode->next = pos->next;pos->next->prev = newnode;//pos的后一个结点的前驱结点指向新节点pos->next = newnode;//pos结点的后继结点指向新结点
}void LTErase(LTNode* pos)
{assert(pos);//保证pos为有效结点pos->prev->next = pos->next;//pos的前一个结点的后继指针指向pos后一个结点pos->next->prev = pos->prev;//pos的后一个结点的前驱指针指向pos的前一个结点free(pos);//释放pospos = NULL;
}void LTDestroy(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;LTNode*next = NULL;while (pcur != phead){next = pcur->next;free(pcur);pcur = next;}//除了头结点都释放完毕free(phead);//phead = NULL;//没有用!
}

六、顺序表和链表的优缺点分析

1、存储空间

顺序表物理上连续

链表逻辑上连续,但是物理上不连续

2、随机访问

顺序表可以通过下标去访问

链表不可以直接通过下标去访问

3、任意位置插入或者删除元素

顺序表需要挪移元素,效率低

链表只需修改指针指向

4、插入

动态顺序表空间不够时需要扩容

链表没有容量的概念

5、应用场景

顺序表应用于元素高效存储+频繁访问的场景

链表应用于任意位置插入和删除频繁的场景

总之:没有绝对的优劣,都要各自适合的应用场景!!

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/651453.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

zabbix使用自动发现批量监控服务器

当有大量新增服务器需要监控时&#xff0c;为避免一台一台手动操作浪费人力&#xff0c;我们使用自动发现功能来进行操作&#xff1a; 以下以zabbix6.4.0版本为例 如下,点击自动发现&#xff0c;创建发现规则&#xff1a; 点击更新&#xff0c;保存&#xff0c;之后点告警---…

在 React 组件中使用 JSON 数据文件,怎么去读取请求数据呢?

要在 React 组件中使用 JSON 数据&#xff0c;有多种方法。 常用的有以下几种方法&#xff1a; 1、直接将 JSON 数据作为一个变量或常量引入组件中。 import jsonData from ./data.json;function MyComponent() {return (<div><h1>{jsonData.title}</h1>&…

node.js Redis SETNX命令实现分布式锁解决超卖/定时任务重复执行问题

Redis SETNX 特性 当然&#xff0c;让我们通过一个简单的例子&#xff0c;使用 Redis CLI&#xff08;命令行界面&#xff09;来模拟获取锁和释放锁的过程。 在此示例中&#xff0c;我将使用键“lock:tcaccount_[pk]”和“status:tcaccount_[pk]”分别表示锁定键和状态键。 获…

AAC解码算法原理

关于更多音视频开发内容&#xff0c;请参考专栏音视频开发 AAC&#xff08;Advanced Audio Coding&#xff09;是一种高级音频编码标准&#xff0c;它是一种十分流行的音频压缩格式&#xff0c;通常用于存储和传输音频数据。AAC提供了高音质和高压缩效率&#xff0c;广泛应用于…

Android源码设计模式解析与实战第2版笔记(四)

第三章 自由扩展你的项目–Builder 模式 Builder 模式的定义 将一个复杂对象的构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 Builder 模式的使用场景 相同的方法&#xff0c;不同的执行顺序&#xff0c;产生不同的事件结果时 多个部件或零件&…

Android 基础技术——Handler

笔者希望做一个系列&#xff0c;整理 Android 基础技术&#xff0c;本章是关于 Handler 为什么一个线程对应一个Looper&#xff1f; 核心&#xff1a;通过ThreadLocal保证 Looper.prepare的时候&#xff0c;ThreadLocal.get如果不空报异常&#xff1b;否则调用ThreadLocal.set,…

Cesium渲染白膜数据

async DrawBaiMoFun2() {// tiles 矩阵变换let changePostion = (tileSet, tx, ty, tz, rx, ry, rz, scale, center) => {if (!center) return;const m = Cesium.Transforms.eastNorthUpToFixedFrame(center);const surface =center ||Cesium.Cartesian3.fromRadians(cartog…

自动驾驶代客泊车AVP决策规划详细设计

背景 随着产品的不断迭代&#xff0c;外部停车场的铺开&#xff0c;PAVP车辆需要应对的场景将越来越复杂&#xff0c;因此整体算法泛化能力的提升显得尤为关键。为了打磨巡航规划的能力&#xff0c;算法架构应当设计的更为灵活&#xff0c;可以针对使用场景迁入更为先进有效的算…

【Linux】分区向左扩容的方法

文章目录 为什么是向左扩容操作前的备份方法&#xff1a;启动盘试用Ubuntu后进行操作 为什么是向左扩容 Linux向右扩容非常简单&#xff0c;无论是系统自带的disks工具还是apt安装的gparted工具&#xff0c;都有图像化的界面可以操作。但是&#xff0c;都不支持向左扩容。笔者…

01 Redis的特性+下载安装启动+Redis自动启动+客户端连接

1.1 NoSQL NoSQL&#xff08;“non-relational”&#xff0c; “Not Only SQL”&#xff09;&#xff0c;泛指非关系型的数据库。 键值存储数据库 &#xff1a; 就像 Map 一样的 key-value 对。如Redis文档数据库 &#xff1a; NoSQL 与关系型数据的结合&#xff0c;最像关系…

免费电视TV盒子软件,好用的免费电视盒子软件大全,免费电视盒子APP大全,2024最新整理

1、TVbox下载地址、影视接口、配置教程 下载地址 TVbox TVbox可用接口地址合集 注&#xff1a;接口均来源于互联网收集分享&#xff01;所有接口都是经过测试的&#xff0c;如果出现加载失败等情况&#xff0c;可能是因为接口针对的盒子有兼容问题&#xff0c;可以多试试几…

Linux中查看端口被哪个进程占用、进程调用的配置文件、目录等

1.查看被占用的端口的进程&#xff0c;netstat/ss -antulp | grep :端口号 2.通过上面的命令就可以列出&#xff0c;这个端口被哪些应用程序所占用&#xff0c;然后找到对应的进程PID https://img-blog.csdnimg.cn/c375eb2bed754426b373907acaa7346e.png 3.根据PID查询进程。…

isctf---web

圣杯战争 php反序列 ?payloadO:6:"summon":2:{s:5:"Saber";O:8:"artifact":2:{s:10:"excalibuer";O:7:"prepare":1:{s:7:"release";O:5:"saber":1:{s:6:"weapon";s:52:"php://filter…

Ubuntu系统中部署C++环境与Visual Studio Code软件

本文介绍在Linux Ubuntu操作系统下,配置Visual Studio Code软件与C++代码开发环境的方法。 在文章VMware虚拟机部署Linux Ubuntu系统的方法中,我们介绍了Linux Ubuntu操作系统的下载、安装方法;本文则基于前述基础,继续介绍在Linux Ubuntu操作系统中配置Visual Studio Code…

【GitHub项目推荐--游戏模拟器(switch)】【转载】

01 任天堂模拟器 yuzu 是 GitHub 上斩获 Star 最多的开源 Nintendo Switch 模拟器 &#xff0c;使用 C 编写&#xff0c;考虑到了可移植性&#xff0c;该模拟器包括 Windows 和 Linux 端。 如果你的 PC 满足必要的硬件要求&#xff0c;该模拟器就能够运行大多数商业游戏&…

Django实战

一、开发登录表单 def login_form(request):html = <html><body><form method="post">用户名:<input name = "username" type="text"></input></br>密码:<input name = "password" type = &q…

CSS 之 图片九宫格变幻效果

一、简介 ​ 本篇博客用于讲解如何实现图片九宫格变幻的样式效果&#xff0c;将图片分为九块填充在33的的九宫格子元素中&#xff0c;并结合grid、hover、transition等CSS属性&#xff0c;实现元素hover时&#xff0c;九宫格子元素合并为一张完整图片的动画效果。 ​ 为了简化…

SpringMVC 环境搭建入门

SpringMVC 是一种基于 Java 的实现 MVC 设计模型的请求驱动类型的轻量级 Web 框架&#xff0c;属于SpringFrameWork 的后续产品&#xff0c;已经融合在 Spring Web Flow 中。 SpringMVC 已经成为目前最主流的MVC框架之一&#xff0c;并且随着Spring3.0 的发布&#xff0c;全面…

02 Redis之配置文件

3. Redis配置文件 3.1 网络部分 首先明确&#xff0c;tcp-backlogestablished Linux 内核 2.2 版本之后&#xff08;现在大部分都是3.x了&#xff09; TCP 系统中维护了两个队列, 用来存放TCP连接 a. SYN_RECEIVED 队列中存放未完成三次握手的连接 b. ESTABLISHED队列中存放已…

Java面试题之序列化和反序列化

Java面试题之序列化和反序列化 文章目录 Java面试题之序列化和反序列化序列化和反序列化什么是序列化?什么是反序列化?如果有些字段不想进行序列化怎么办&#xff1f;常见序列化协议有哪些&#xff1f;为什么不推荐使用 JDK 自带的序列化&#xff1f; 文章来自Java Guide 用于…