目录

链表的分类

带头双向循环链表的概念及结构

概念

结构

带头双向循环链表的基本操作

初始化ListInit

尾插ListPushBack

头插ListPushFront

尾删ListPopBack

​头删ListPopFront

插入ListInsert

删除ListErase

查找ListFind

修改ListModify

打印ListPrint

长度ListSize

销毁ListDestroy

判空ListEmpty

动态顺序表与带头双向循环链表的比较

代码


链表的分类

链表的种类非常丰富,有带头结点的或不带头结点的,有单向的或双向的,有循环的或非循环的,这样组合起来有2*2*2=8种链表结构。

虽然有这么多的链表结构,但是我们实际中最常用的还是两种结构:

最后我们再了解一下头结点与头指针的概念:不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头链表中第一个结点,该结点通常不存储数据。

引入头结点可以带来两个优点:1.由于第一个数据结点的位置被存储在头结点的指针域中,因此在链表的第一个位置上的操作和在链表其他位置上的操作一致,无须进行特殊处理。2.无论链表是否为空,其头指针都指向头结点,因此空表与非空表的处理也得到统一。


带头双向循环链表的概念及结构

概念

链表是一种物理结构上非连续、非顺序的存储结构,数据元素的逻辑结构顺序是通过链表
中的指针链接次序实现的 。

而带头双向循环链表是链表组合中最复杂的结构。

  • 因为是双向的,所以每个结点中有prev指针与next指针,其prev指针指向其结点的前一个结点,next指针指向其结点的后一个结点。
  • 并且拥有头结点,其头结点的next指向第一个数据结点,prev指向最后一个数据结点。
  • 又因为是循环结构,所以最后一个数据结点的next指向头结点。

结构

带头双向循环链表的结点需要存储前一个结点地址的指针prev,存储后一个结点地址的指针next以及存储数据的data,这样的带头双向循环链表的结点结构才算完整。


带头双向循环链表的基本操作

初始化ListInit

带头双向循环链表的初始化操作本质上就是开辟一个头结点并将头指针指向开辟好的头结点。    有两种实现方法:1.将头指针传给ListInit在函数体内把头指针的指向改为头结点,所以需要用到二级指针,传头指针的地址才能改变头指针的指向。2.直接将开辟好的头结点地址作为返回值来改变头指针的指向。

在这之前我们先实现一个开辟结点的函数,并将开辟好的结点中的prev和next指向自己。

尾插ListPushBack

尾插就是在链表的最后一个数据结点的后面增添一个新结点,本质上就是将最后一个数据结点的next指向newNode,而newNode的prev指向最后一个数据结点,因为是循环结构,还需将头结点的prev指向newNode,而newNode的next指向头结点。

与单链表相比,我们不需要遍历寻找最后一个数据结点,因为头结点的prev就是指向最后一个数据结点。

当链表为空时,其操作也是一样的:

头插ListPushFront

头插的操作就是将第一个数据结点的prev指向newNode,而newNode的next指向第一个数据结点,头结点的next指向newNode,而newNode的prev指向头结点。

当链表为空时,其操作也是一样的。

尾删ListPopBack

尾删的操作就是将最后一个数据结点的前一个结点的next指向头结点,而头结点的prev指向最后一个数据结点的前一个结点,然后释放掉最后一个数据结点。如果链表只剩下一个数据结点进行删除的话,操作也是一样的,尾删完最后一个结点就会回到头结点的next与prev指向自己的场景。

头删ListPopFront

头删的操作就是将第一个数据结点的下一个结点的prev指向头结点,而头结点的next指向第一个数据结点的下一个结点,然后释放掉第一个数据结点。如果链表只剩下一个数据结点进行删除的话,操作也是一样的,头删完最后一个结点就会回到头结点的next与prev指向自己的场景。

插入ListInsert

插入就是在pos位置前插入一个新结点,因为该链表为带头双向循环链表,所以我们可以直接找到pos位置的前一个结点,而不需要遍历找pos位置的前一个结点。具体操作就是将pos位置的前一个结点的next指向newNode,而newNode的prev指向pos位置的前一个结点,再将pos的prev指向newNode,而newNode的next指向pos即可完成插入操作。

    实现插入操作后,我们的尾插与头插可以复用插入操作:

删除ListErase

删除就是将pos位置的结点释放掉。具体操作就是将pos位置的前一个结点的next指向pos位置的后一个结点,而pos位置的后一个结点的prev指向pos位置的前一个结点,然后释放掉pos位置的结点。

    实现删除操作后,我们的尾删与头删可以复用删除操作:

查找ListFind

按值查找就是将链表遍历一遍,并将查到存储该值的结点地址返回。

修改ListModify

修改操作就是将pos位置的data数据改为x数据。

将链表中所有存储2的结点改为存储0:

打印ListPrint

之前单链表遍历的结束条件是curr是否等于NULL,因为最后一个数据结点的next指向NULL。而带头双向循环链表的遍历结束条件是curr是否等于phead,因为最后一个数据结点的next指向phead。 

长度ListSize

求大小的操作同样是将链表遍历一遍,通过计数来求链表的长度。

另一种求链表长度的方法: 将头指针与一个整形size封装到一个结构体中,当链表增加数据时size++,若链表减少数据时size--这样就不需要遍历。

销毁ListDestroy

销毁的操作就是反复调用ListErase将所有的数据结点释放,最后将头结点也释放掉,调用ListDestroy之后记得将头指针置空。

判空ListEmpty

如果带头双向循环链表为空返回true,不为空返回false;当头结点的next与prev都指向自身则说明链表为空。


动态顺序表与带头双向循环链表的比较

动态顺序表

  • 优点:支持随机访问,缓存利用率高
  • 缺点:头部或者中间位置插入删除效率低,扩容有一定程度的性能消耗,也可能存在一定程度的空间浪费
  • 应用场景:频繁访问,高效存储

带头双向循环链表

  • 优点:任意位置插入和删除的时间复杂度为O(1),按需申请释放
  • 缺点:不支持随机访问,缓存利用率低,会造成一定的内存碎片
  • 应用场景:频繁的任意位置插入删除

代码

// 头文件
typedef int ListDataType; // 数据的类型typedef struct ListNode
{struct ListNode* prev; // 指向前一个结点 ListDataType data; // 存储数据struct ListNode* next; // 指向后一个结点
}ListNode;struct List
{ListNode* phead; // 头指针int size; // 记录长度
};//void ListInit(ListNode** pphead);
ListNode* ListInit(); // 初始化
void ListPushBack(ListNode* phead,ListDataType x); // 尾插
void ListPushFront(ListNode* phead, ListDataType x); // 头插
void ListPopBack(ListNode* phead); // 尾删
void ListPopFront(ListNode* phead); // 头删
void ListPrint(ListNode* phead); // 打印
void ListInsert(ListNode* pos,ListDataType x); // 任意位置插入
void ListErase(ListNode* pos); // 任意位置删除
ListNode* ListFind(ListNode* phead,ListDataType x); // 按值查找
void ListModify(ListNode* pos, ListDataType x); // 修改
int ListSize(ListNode* phead); // 求长度
void ListDestroy(ListNode* phead); // 销毁
bool ListEmpty(ListNode* phead); //判空//源文件
void ListPrint(ListNode* phead) // 遍历打印带头双向循环链表
{assert(phead);ListNode* curr = phead->next;while (curr != phead){printf("%d ", curr->data);curr = curr->next;}printf("\n");
}//void ListLnit(ListNode** pphead) // 链表的初始化---创建头结点并让头指针指向头结点
//{
//  assert(pphead);
//
//  ListNode* newNode = BuyListNode(0);
//
//  *pphead = newNode;
//}ListNode* BuyListNode(ListDataType x) // 在堆上创建一个结点
{ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));if (!newNode){perror("malloc");exit(-1);}newNode->data = x;newNode->prev = newNode; // 让结点中的prev与next指向自己newNode->next = newNode;return newNode; // 返回创建成功的结点的地址
}ListNode* ListInit() // 链表的初始化---创建头结点并返回头结点的地址
{ListNode* newNode = BuyListNode(0);return newNode;
}void ListPushBack(ListNode* phead, ListDataType x) // 尾插
{assert(phead);ListInsert(phead, x); // 尾插/*ListNode* newNode = BuyListNode(x);ListNode* tail = phead->prev;tail->next = newNode;newNode->prev = tail;phead->prev = newNode;newNode->next = phead;*//*ListNode* newNode = BuyListNode(x);phead->prev->next = newNode;newNode->prev = phead->prev;phead->prev = newNode;newNode->next = phead;*/
}void ListPushFront(ListNode* phead, ListDataType x) // 头插
{assert(phead);ListInsert(phead->next, x); // 头插/*ListNode* newNode = BuyListNode(x);ListNode* temp = phead->next;temp->prev = newNode;newNode->next = temp;phead->next = newNode;newNode->prev = phead;*//*ListNode* newNode = BuyListNode(x);phead->next->prev = newNode;newNode->next = phead->next;phead->next = newNode;newNode->prev = phead;*/
}void ListPopBack(ListNode* phead) // 尾删
{assert(phead);if (phead->next == phead && phead->prev == phead) // 如果链表为空不需要删除{assert(0);return;}ListErase(phead->prev); // 尾删/*ListNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;free(del);*//*ListNode* del = phead->prev;phead->prev->prev->next = phead;phead->prev = phead->prev->prev;free(del);*/
}void ListPopFront(ListNode* phead) // 头删
{assert(phead);if (phead->next == phead && phead->prev == phead) // 链表为空则不需要删除{assert(0);return;}ListErase(phead->next); // 头删/*ListNode* del = phead->next;del->next->prev = phead;phead->next = del->next;free(del);*//*ListNode* del = phead->next;phead->next->next->prev = phead;phead->next = phead->next->next;free(del);*/
}void ListInsert(ListNode* pos, ListDataType x) // 插入---在pos位置前插入
{assert(pos);ListNode* newNode = BuyListNode(x);ListNode* prev = pos->prev; prev->next = newNode;newNode->prev = prev;pos->prev = newNode;newNode->next = pos;/*pos->prev->next=newNode;newNode->prev=pos->prev;pos->prev=newNode;newNode->next=pos;*/
}void ListErase(ListNode* pos) // 删除---删除pos位置的结点
{assert(pos); // 不需要额外检查链表是否为空,因为pos存在就证明链表不为空。ListNode* prev = pos->prev;ListNode* next = pos->next;prev->next = next;next->prev = prev;free(pos);/*pos->prev->next=pos->next;pos->next->prev=pos->prev;free(pos);*/
}ListNode* ListFind(ListNode* phead, ListDataType x) // 按值查找
{assert(phead);ListNode* curr = phead->next;while (curr != phead) {if (curr->data == x)return curr;curr = curr->next;}return NULL; // 没有找到返回空指针
}void ListModify(ListNode* pos, ListDataType x) // 修改---将pos位置的data数据改为x
{assert(pos);pos->data = x;
}int ListSize(ListNode* phead) // 求长度(元素个数)
{assert(phead);int size = 0;ListNode* curr = phead->next;while (curr != phead){size++;curr = curr->next;}return size;
}void ListDestroy(ListNode* phead) // 销毁
{assert(phead);ListNode* curr = phead->next;while (curr != phead){ListNode* next = curr->next;ListErase(curr);curr = next;}free(phead);
}bool ListEmpty(ListNode* phead) // 判断带头双向循环链表是否为空
{assert(phead);return phead->next == phead && phead->prev == phead; // 为空返回true否则返回false
}

数据结构-带头双向循环链表相关推荐

  1. 数据结构-带头双向循环链表(增删查改详解)

    在上一篇博客中,详细介绍了单链表的增删查改,虽然单链表的结构简单,但是用起来却不是那么顺手.因此根据单链表的种种缺点,这篇博客所介绍的带头双向循环链表将会带来极大的优化. 上图就是带头双向循环链表的主 ...

  2. 数据结构与算法 | 带头双向循环链表

    上一节里实现的是最简单的链表,在实际中那种链表不会单独用来存储数据,更多是作为其他数据结构的子结构,如图的邻接表等.而比较常用的就是带头双向循环链表. 通过对比我们可以看出有三个不同,多了头节点,链表 ...

  3. 比特数据结构与算法(第二章收尾)带头双向循环链表的实现

    1.链表的分类 链表的分类 ① 单向或者双向 ② 带头或者不带头 ③ 循环或者非循环 常用的链表: 根据上面的分类我们可以细分出8种不同类型的链表,这么多链表我们一个个讲解这并没有意义.我们实际中最常 ...

  4. 【数据结构】链表:带头双向循环链表的增删查改

    本篇要分享的内容是带头双向链表,以下为本片目录 目录 一.链表的所有结构 二.带头双向链表 2.1尾部插入 2.2哨兵位的初始化 2.3头部插入 2.4 打印链表 2.5尾部删除 2.6头部删除 2. ...

  5. 初阶数据结构之带头+双向+循环链表增删查实现(三)

    文章目录 @[TOC](文章目录) 前言 一.带头双向循环链表的初始化 1.1带头双向循环链表的结构体定义 1.2初始化代码的实现 二.带头+双向+循环链表的增功能实现 2.1头插代码的实现 2.2尾 ...

  6. 数据结构:带头双向循环链表——增加、删除、查找、修改,详细解析

    读者可以先阅读这一篇:数据结构--单链表的增加.删除.查找.修改,详细解析_昵称就是昵称吧的博客-CSDN博客,可以更好的理解带头双向循环链表. 目录 一.带头双向循环链表的处理和介绍 1.带头双向循 ...

  7. 【数据结构】-关于带头双向循环链表的增删查改

    作者:低调 作者宣言:写好每一篇博客 文章目录 前言 一.带头双向循环链表的实现 1.1创建返回链表的头结点 1.2开辟一个新的结点 1.3双向链表的销毁 1.4双向链表的打印 1.5双向链表尾插 1 ...

  8. 【数据结构初阶】链表(下)——带头双向循环链表的实现

    目录 带头双向循环链表的实现 1.带头双向循环链表的节点类型 2.创建带头双向循环链表的节点 3.向带头双向循环链表中插入数据 <3.1>从链表尾部插入数据 <3.2>从链表头 ...

  9. 【数据结构】双向链表(带头双向循环链表)——超详细代码

    文章目录 1. 双链表 1.1 前言 1.2 带头双向循环链表 2. 带头双向循环链表的实现 2.1 双向链表的定义声明 2.2 双向链表的初始化 2.3 释放双向链表 2.4 打印双向链表 2.5 ...

  10. 【数据结构】带头双向循环链表

    各位读者们好久不见了,咋们接着上一期链表来,今天来实现一下链表最难的结构,同时也是实现起来最简单的结构--带头双向循环链表.话不多说,进入主题 文章目录 前言 实现带头双向循环链表 DList.h头文 ...

最新文章

  1. CLI4 去掉严格模式
  2. Git undo 操作
  3. zookeeper版本更新_ZooKeeper入门,看这篇就够了!
  4. 佳能单反相机二次开发包介绍_家用单反相机什么牌子好
  5. python无法调用安装的包_如何使用python命令和安装包进行安装Django框架
  6. 批量刷新远程物化视图的方法(备用)
  7. VS2019C++代码出现cout不明确
  8. 用计算机程序求n,计算机编程 算法 求n!.doc
  9. 微型计算机2014年9月下,2014年9月计算机应用基础》网络教育统考真题.doc
  10. 电脑族每天宜喝四杯茶
  11. 驱动开发入门 - 之一:Win7 SP1 x64 驱动开发环境搭建
  12. 信息学奥赛C++编程:计算分数加减表达式的值
  13. 飞秋FeiQ2013特色功能列表
  14. Weakly Supervised Instance Segmentation using the Bounding Box Tightness Prior 论文笔记
  15. LOJ#2833 「JOISC 2018 Day 1」帐篷 dp
  16. ios和android 浏览器适配问题总结
  17. java设置打印机默认纸张_java 打印设置打印A4 A5纸
  18. 华为服务器gpu芯片,GPU服务器推荐
  19. Android中自带的list布局
  20. WIN7/WIN10 临时及永久 强制关闭驱动签名验证

热门文章

  1. 使用虚幻4进行安卓打包
  2. Anaconda3安装及opencv配置
  3. “十三五”输电通道规模倍增 重点建设配电网
  4. 计算机打印指定测试页到文件夹中,打印机可以打测试页,但不能打印别的文件,怎么处理...
  5. 我的世界php motd,我的世界motd标语编辑器
  6. 【Excel 教程系列第 17 篇】如何根据图片名称自动查找出对应的图片
  7. Python 音频调整音量(附代码) | Python工具
  8. ajax调用一般应用程序,【Web前端】---js调用本地应用程序
  9. 千月双端影视app源码 后端是Thinkphp
  10. NBU 备份系统详细操作手册之维护与安装