数据结构线性表——带头双向循环链表

前言:小伙伴们好久不见啦,上篇文章我们一起学习了数据结构线性表其一的单链表,了解了单链表的不少好处,但是不可能有完美的数据结构,就算是单链表,也会有很多缺点。

那么今天这篇文章,我们就来学习单链表的promax版本——带头双向循环链表


一.什么是带头双向循环链表

关于带头双向循环链表,我们将它拆分为带头、双向、循环、链表四个部分,其中链表我们已经知道是怎么回事了,那我们就来一起结合下图分析前三个概念。

1.带头 

        所谓带头,也就是在链表的开头处,有一个不存放任何数据的头节点,我们通常称其为“哨兵位”。

        那么哨兵位存在的意义是什么呢???

        它可以帮助我们更方便的进行对链表的各种操作。具体好在哪里,我们结合后边实现链表的各种操作来进行展示。

2.双向

        我们前边学习过的单链表,它的每个节点之间只有一条链子相连,并且只能由前一个节点去找到后一个节点

        而双向链表,也就是两个节点之间有两条链子相连,不仅能从前一个找到后一个,也能从后一个去找到前一个

3.循环

        循环,顾名思义,就是将链表的头尾也进行连接,形成一个逻辑意义上的环形链表。

那么理解完带头双向循环链表的含义之后,我就就一起来看看到底来如何实现它吧。

此后我们将该链表的名字简化为双链表


二.双链表的实现

1.双链表的定义

typedef int DLLDataType;
//定义双链表
typedef struct DLinkList
{DLLDataType data;struct DLinkList* prev;//指向前一个节点struct DLinkList* next;//指向后一个节点
}DLLNode;

双链表是在单链表的基础上,比它多出一个prev指针去指向前一个节点,还是比较容易理解的。


2.双链表的初始化

//初始化双链表
DLLNode* DLinkListInit()
{DLLNode* phead = (DLLNode*)malloc(sizeof(DLLNode));if (phead == NULL){perror("DLinkListInit->malloc");}phead->next = phead;phead->prev = phead;return phead;
}

双链表的初始化需要先造出哨兵位考虑到链表为空,并且链表还要循环,所以我们将哨兵位的prev和next都指向自己

    DLLNode* dll = DLinkListInit();

创建一个双链表,我们习惯于运用上述方式。

因为如果用单链表的初始化方式,我们需要用到二级指针,但是我们后续双链表各种功能的操作,完全不和二级指针沾边

所以为了让我们的双链表全部由一级指针完成,选择采用接收函数返回值的方式来创建双链表


3.双链表节点的创建

DLLNode* CreateNewNode(DLLDataType x)
{DLLNode* newnode = (DLLNode*)malloc(sizeof(DLLNode));if (newnode == NULL){perror("CreateNewNode->malloc");}newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode;
}

双链表创建新节点就和单链表差不多啦,要注意的就是不要忘记两个指针置空,防止出现野指针

这样,我们就实现了一个基本的双链表框架,下面来实现双链表的各种基础操作。


 三.双链表的操作

1.双链表的打印

那么为了方便其他功能的测试,我们还是先来实现双链表的打印功能:

void DLinkListPrint(DLLNode* phead)
{assert(phead);DLLNode* cur = phead->next;printf("phead<=>");while (cur != phead){printf("%d<=>", cur->data);cur = cur->next;}printf("phead\n");
}

我们还是严格的进行一下assert断言如果phead为空,就说明双链表不存在

这里要注意两点:

1.cur为什么是phead->next???

        不难理解,我们在双链表初始化的时候,给到dll的返回值是哨兵位,但是哨兵位不存储数据,所以要从哨兵位的下一个节点开始。

2.while循环的判断条件

        因为我们是一个可循环的链表,所以并不存在cur为空的情况,但是cur最后会重新指向哨兵位,所以当cur == phead时,说明我们已经将双链表遍历一遍了

至于printf函数的内容,只是为了好看哈哈,展示一下:

这样能够让大家更形象的认识双链表。


2.双链表的尾插

双链表的尾插相较于单链表有什么优势呢???

单链表想尾插,首先要进行循环找尾时间复杂度就高了,但是双链表就好办,因为哨兵位的前一个节点就是尾,也就是phead->prev,尾找到之后,就好办了:

//尾插
void DLinkListPushBack(DLLNode* phead, DLLDataType x)
{assert(phead);DLLNode* newnode = CreateNewNode(x);DLLNode* tail = phead->prev;tail->next = newnode;newnode->next = phead;newnode->prev = tail;phead->prev = newnode;
}

用tail代替尾,接下来的一顿操作,就是:

旧尾的next指向新尾

新尾的next指向哨兵位

新尾的prev指向旧尾

哨兵位的prev指向新尾

看起来很简单,但是我们知道,单链表必须得考虑一下链表是否为空的特例,但是双链表不需要

因为双链表如果为空,那就只有哨兵位,哨兵位自己的头尾相连,带入上述代码操作之后,不会有任何错误。


 3.双链表的尾删

尾删就更简单了,只需要找到尾,再通过尾找到尾的前一个节点,再让此节点和哨兵位互连,再将尾free即可:

//尾删
void DLinkListPopBack(DLLNode* phead)
{assert(phead);assert(phead->next != phead);DLLNode* tail = phead->prev;DLLNode* tailprev = tail->prev;phead->prev = tailprev;tailprev->next = phead;free(tail);tail = NULL;
}

尾删要考虑只有一个节点的特例吗,依然不用,因为进行一顿操作之后,还是让哨兵位自己头尾相连

但是尾删要考虑空链表的情况,因为如果链表为空,free的就是哨兵位了,哨兵位一旦不存在了,我们就无法进行后续的操作了。所以要多进行一次assert断言。

到这里,小伙伴们是否已经感受到了哨兵位,以及双链表的强势之处啦


4.双链表的头插

头插就和尾插差不多了,这里我直接给出代码,希望小伙伴们可以自己理解掌握哦。

//头插
void DLinkListPushFront(DLLNode* phead, DLLDataType x)
{assert(phead);DLLNode* head = phead->next;DLLNode* newnode = CreateNewNode(x);phead->next = newnode;newnode->next = head;head->prev = newnode;newnode->prev = phead;
}

5.双链表的头删

头删也和尾删类似,要考虑空链表的情况:

//头删
void DLinkListPopFront(DLLNode* phead)
{assert(phead);assert(phead->next != phead);DLLNode* head = phead->next;DLLNode* headnext = head->next;phead->next = headnext;headnext->prev = phead;free(head);head = NULL;
}

6.双链表的查找

如果是查找的话,那我们还得老老实实的从头遍历:

//查找
DLLNode* DLinkListFind(DLLNode* phead,DLLDataType x)
{assert(phead);DLLNode* cur = phead->next;while(cur){if (cur->data == x)return cur;elsecur = cur->next;}return NULL;
}

还是要注意这里while循环的条件,和双链表的打印一样


7.双链表的任意插

双链表的任意位置的插入依然要和查找连用,因为只有查找才能得到pos位置的地址

但是我们这里规定一下,任意插就是pos位置前插

比如说我想在表的第四个位置插入新数据,那我就要把第四个位置空出来,让原来的第四位以及他后边的都老老实实往后退

这样一来,我们就需要找到pos节点的前一个节点,这样方便我们进行操作:

//pos位置插
void DLinkListInsert(DLLNode* pos, DLLDataType x)
{assert(pos);DLLNode* newnode = CreateNewNode(x);DLLNode* posprev = pos->prev;posprev->next = newnode;newnode->prev = posprev;pos->prev = newnode;newnode->next = pos;
}

8.双链表的任意删

任意删的形式就和任意插差不多,只是还需要另外记录pos的下一个节点

//pos位置删
void DLinkListEease(DLLNode* pos)
{assert(pos);DLLNode* posprev = pos->prev;DLLNode* posnext = pos->next;posprev->next = posnext;posnext->prev = posprev;free(pos);pos = NULL;
}

9.双链表的修改

想要修改数据,还是要用查找操作来找到要修改pos位置的地址,而后就简单了:

//pos位置改
void DLinkListAmend(DLLNode* pos, DLLDataType x)
{assert(pos);pos->data = x;
}

直接修改data即可。


10.双链表的销毁

双链表的销毁,同样是需要遍历对个个空间进行free,值得注意的是,哨兵位也需要销毁

//销毁
void DLinkListDestroy(DLLNode* phead)
{assert(phead);DLLNode* cur = phead->next;while (cur != phead){DLLNode* next = cur->next;free(cur);cur = next;}free(phead);phead = NULL;
}

四.完整代码展示

1.DLinkList.h

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>typedef int DLLDataType;
//定义双链表
typedef struct DLinkList
{DLLDataType data;struct DLinkList* prev;struct DLinkList* next;
}DLLNode;//初始化双链表
DLLNode* DLinkListInit();
//打印双链表
void DLinkListPrint(DLLNode* phead);
//创造新节点
DLLNode* CreateNewNode(DLLDataType x);
//尾插
void DLinkListPushBack(DLLNode* phead, DLLDataType x);
//尾删
void DLinkListPopBack(DLLNode* phead);
//头插
void DLinkListPushFront(DLLNode* phead, DLLDataType x);
//头删
void DLinkListPopFront(DLLNode* phead);
//查找
DLLNode* DLinkListFind(DLLNode* phead,DLLDataType x);
//pos位置插
void DLinkListInsert(DLLNode* pos, DLLDataType x);
//pos位置删
void DLinkListEease(DLLNode* pos);
//pos位置改
void DLinkListAmend(DLLNode* pos,DLLDataType x);
//销毁
void DLinkListDestroy(DLLNode* phead);

2.DLinkList.c

#include "DLinkList.h"
//初始化双链表
DLLNode* DLinkListInit()
{DLLNode* phead = (DLLNode*)malloc(sizeof(DLLNode));if (phead == NULL){perror("DLinkListInit->malloc");}phead->next = phead;phead->prev = phead;return phead;
}
//打印双链表
void DLinkListPrint(DLLNode* phead)
{assert(phead);DLLNode* cur = phead->next;printf("phead<=>");while (cur != phead){printf("%d<=>", cur->data);cur = cur->next;}printf("phead\n");
}
//创造新节点
DLLNode* CreateNewNode(DLLDataType x)
{DLLNode* newnode = (DLLNode*)malloc(sizeof(DLLNode));if (newnode == NULL){perror("CreateNewNode->malloc");}newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode;
}
//尾插
void DLinkListPushBack(DLLNode* phead, DLLDataType x)
{assert(phead);DLLNode* newnode = CreateNewNode(x);DLLNode* tail = phead->prev;tail->next = newnode;newnode->next = phead;newnode->prev = tail;phead->prev = newnode;
}
//尾删
void DLinkListPopBack(DLLNode* phead)
{assert(phead);DLLNode* tail = phead->prev;DLLNode* tailprev = tail->prev;phead->prev = tailprev;tailprev->next = phead;free(tail);tail = NULL;
}
//头插
void DLinkListPushFront(DLLNode* phead, DLLDataType x)
{assert(phead);DLLNode* head = phead->next;DLLNode* newnode = CreateNewNode(x);phead->next = newnode;newnode->next = head;head->prev = newnode;newnode->prev = phead;
}
//头删
void DLinkListPopFront(DLLNode* phead)
{assert(phead);DLLNode* head = phead->next;DLLNode* headnext = head->next;phead->next = headnext;headnext->prev = phead;free(head);head = NULL;
}
//查找
DLLNode* DLinkListFind(DLLNode* phead,DLLDataType x)
{assert(phead);DLLNode* cur = phead->next;while(cur){if (cur->data == x)return cur;elsecur = cur->next;}return NULL;
}
//pos位置插
void DLinkListInsert(DLLNode* pos, DLLDataType x)
{assert(pos);DLLNode* newnode = CreateNewNode(x);DLLNode* posprev = pos->prev;posprev->next = newnode;newnode->prev = posprev;pos->prev = newnode;newnode->next = pos;
}
//pos位置删
void DLinkListEease(DLLNode* pos)
{assert(pos);DLLNode* posprev = pos->prev;DLLNode* posnext = pos->next;posprev->next = posnext;posnext->prev = posprev;free(pos);pos = NULL;
}
//pos位置改
void DLinkListAmend(DLLNode* pos, DLLDataType x)
{assert(pos);pos->data = x;
}
//销毁
void DLinkListDestroy(DLLNode* phead)
{assert(phead);DLLNode* cur = phead->next;while (cur != phead){DLLNode* next = cur->next;free(cur);cur = next;}free(phead);phead = NULL;
}

测试代码大家自行进行测试,这里就不在进行展示啦。


五.总结

双链表相比于单链表还是有很大优势的,建议大家在学习过单链表的基础上完全靠自己的写一写双链表,这将会让你对链表知识的掌握更上一层楼!

最后还是提醒大家不要忘记一键三连哦!!!

我们下期再见啦!

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

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

相关文章

【debug】解决Kali虚拟机开机黑屏,左上角光标一直闪动无法开机问题

做网络攻防实验时&#xff0c;突然Kali无法打开&#xff0c;遇到这个问题。。。。。。 遇到的问题 突然kali虚拟机变成如下黑屏&#xff0c;无法开机&#xff0c;左上角光标闪动&#xff0c;重启无效。 解决办法 在上图界面&#xff0c;按Ctrl F3&#xff08;不同电脑快捷键…

vue-组件注册及使用

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Vue篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来vue篇专栏内容-组件注册及使用 目录 1、组件的注册及使用 2、组件常用属性 2.1、directive 2.2、computed 2.…

ssh开启,centOS7

1、先确定虚拟机是否装了openssh-server&#xff0c;执行 yum list installed |grep openssh-server 查看是否安装 [rootlocalhost ~]# yum list installed |grep openssh-server Repodata is over 2 weeks old. Install yum-cron? Or run: yum makecache fast openssh-serve…

算法通关村第十五关白银挑战——海量数据场景下的热门算法题

大家好&#xff0c;我是怒码少年小码。 最近超级忙&#xff0c;很多实验报告&#xff0c;已经四五天没搞了&#xff0c;但是我还是回来了&#xff01; 海量数据场景下的热门算法题 本篇的题目不要求写代码&#xff0c;面试的时候能很清楚的说出思路就可以了。 1. 从40个亿中…

RESTful API概述以及如何使用它构建 web 应用程序

REST&#xff08;Representational State Transfer&#xff09;是一种设计风格和架构原则&#xff0c;它是一种为 Web 应用程序提供简化和标准化的 API 的方式。RESTful API&#xff08;RESTful Web Services&#xff09;是符合 REST 架构风格的网络应用程序 API&#xff0c;它…

如何解决Windows电脑 Create folder error,Access is denied.

如何解决 Create folder error, Error: mkdir C:\Program Files\nodejs\21.1.0/: Access is denied. Waring: Name : http://npm.taobao.org/mirrors/node/v21.1.0/win-x64/node.exe Code : -2 Error : Create folder error, Error: mkdir C:\Program Files\nodejs\\21.1.0/…

【文末送书】如何在时间循环里最优决策?

欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和技术。关…

OpenAI调查ChatGPT故障;向量搜索的优势与局限

&#x1f989; AI新闻 &#x1f680; OpenAI调查ChatGPT故障&#xff0c;发布新AI产品GPTs和GPT-4 Turbo 摘要&#xff1a;OpenAI的ChatGPT和其他服务出现故障&#xff0c;经过调查后发现是由于DDoS攻击导致的异常流量模式。OpenAI在首届开发者大会上发布了新的AI产品GPTs&am…

CSS3 2D、3D转换

一、CSS3 2D转换&#xff1a; CSS3转换可以对元素进行移动、缩放、转动、拉长或拉伸。 2D变换的方法&#xff1a;translate()、rolate()、scale()、skew()、matrix()。 <style> div { width:200px; height:100px; background-color:red; /* Rotate div */ tran…

酷柚易汛ERP - 发货地址管理操作指南

1、应用场景 对发货地址进行管理&#xff0c;使用【物流服务】时的自动获取发货地址。 2、主要操作 打开【资料】-【发货地址管理】新增发货地址。 可以对进行地址设置及管理&#xff0c;点击【新增】可添加新的发货地址信息地址简称方便使用者在选择发货地址时&#xff0c;…

Lambertian模型(完美漫反射)

这里使用相乘的方式组合光照色和纹理色。根据这个模型,面朝光源的区域光照强度高,纹理色也相应增强。面背光源的区域光照弱,纹理色也被抑制。这样通过光照和纹理的结合,可以合成出具有照明效果的面部颜色,而不仅仅是固定的纹理本身的颜色。相乘方式可以近似实现不同光照方向下面…

成都优优聚美团代运营:打造高效电商运营的利器

一、引人注目的标题 在繁杂的电商市场中&#xff0c;成都优优聚美团代运营以其专业的服务&#xff0c;为商家提供了一站式的解决方案。那么&#xff0c;这个备受瞩目的代运营平台有何特别之处呢&#xff1f;今天&#xff0c;我们就来一探究竟。 二、平台背景与优势 成都优优聚…

JL-03小型气象站气象环境在线监测设备自动上传并保存数据

JL-03小型气象站产品概述 小型气象站用于对风速、风向、雨量、空气温度、空气湿度、太阳辐射、光照强度、土壤温度、土壤湿度、蒸发量、大气压力等气象要素进行现场监测。既可以通过无线通讯将数据传送至云平台&#xff0c;又可以通过配套的数据采集通讯线与计算机进行连接&am…

1. 深度学习——激活函数

机器学习面试题汇总与解析——激活函数 本章讲解知识点 什么是激活函数&#xff1f; 为什么要使用激活函数&#xff1f; 详细讲解激活函数 本专栏适合于Python已经入门的学生或人士&#xff0c;有一定的编程基础。本专栏适合于算法工程师、机器学习、图像处理求职的学生或人…

一分钟秒懂人工智能对齐

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

Javaweb之javascript的小案例的详细解析

1.5.4 案例 1.5.4.1 需求说明 鲁迅说的好&#xff0c;光说不练假把式,光练不说傻把式。所以接下来我们需要通过案例来加强对于上述DOM知识的掌握。需求如下3个&#xff1a; 点亮灯泡 将所有的div标签的标签体内容后面加上&#xff1a;very good 使所有的复选框呈现被选中的…

AI毕业设计生成器(基于AI大模型技术开发)

这是一个辅助生成计算机毕业设计的工具&#xff0c;可以自动完成毕业设计的源码。它基于几百个github上面开源的java和python项目&#xff0c;运用tengsorflow技术&#xff0c;训练出了AI大模型。基本实现了计算机毕业设计生成器&#xff0c;能够初步生成Java或python基本源码。…

钉钉API与集简云无代码开发连接:电商平台与营销系统的自动化集成

连接科技与能源&#xff1a;钉钉API与集简云的一次集成尝试 在数字化时代&#xff0c;许多公司面临着如何将传统的工作方式转变为更智能、高效的挑战。某能源科技有限公司也不例外&#xff0c;他们是一家专注于能源科技领域的公司&#xff0c;产品包括节能灯具、光伏逆变器、电…

前端面试题之vue篇

vue基础 vue的基本原理 当一个Vue实例创建时&#xff0c;Vue会遍历data中的属性&#xff0c;用Object.defineProperty(Vue使用proxy)转换为getter/setter&#xff0c;并且在内部追踪相关依赖&#xff0c;在属性被访问和修改时通知变化。每个组件实例都有相应的watcher程序实例…

BM65 最长公共子序列(二)

动态规划 BM65 最长公共子序列&#xff08;二&#xff09; 这道题是动态规划的典型例题。 思路 题目要求获取最长公共子序列&#xff0c;我们要先求最长公共子序列的长度&#xff0c;然后根据这个长度倒推从而获取这个子序列。注意&#xff1a;子序列不是子串&#xff0c;子…