本篇要分享的内容是带头双向链表,以下为本片目录

目录

一、链表的所有结构

二、带头双向链表

2.1尾部插入

2.2哨兵位的初始化

2.3头部插入

2.4 打印链表

2.5尾部删除

2.6头部删除

2.7查找结点

2.8任意位置插入

2.9任意位置删除


在刚开始接触链表的时候,我们所学仅仅所学的是单链表,相信大家用C语言学习单链表时也倍受二级指针的折磨。当然单链表只是链表结构内的一种,他的结构非常简单,但是理解并操作起来却非常困难;而我们今天要研究的是链表中结构最复杂,但是理解起来最简单的链表的结构。

一、链表的所有结构

在学习带头双向链表之前先了解一下链表的所有结构

1.单向或双向

2.带头或不带头

3.循环或不循环

还可以将以上的链表结构进行组合

最终链表有八种结构。

这里要说明的是带头和不带头的情况,这里的头意思就是哨兵位,哨兵位也就是作为链表的开头链接后面的数据,但是不存放任何数据,需要单独开辟一个结点来确定哨兵位,这就是带头不带头的意思。

二、带头双向链表

其实我们也没有必要一个一个的去了解那么多的链表结构,我们平时用到的最多的还是两个结构

1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。

2.1尾部插入

和单链表相同,我们同样掌握带头循环链表的增删查改,但是带头循环链表的增删查改会比单链表容易许多。

首先定义一个结构体来存放前一个结点的位置和后一个结点的位置和数据;

typedef int LTDataType;
typedef struct ListNode
{struct ListNode* prev;struct ListNode* next;LTDataType Data;
}LTNode;

接下来画图给大家演示一下

这里要插入的是newnode这个结点,我们可以直接操作phead的prev来实现尾插。

和单链表的尾插相同的,尾插就得先找尾,这个链表结构中的找尾,只需要通过phead的prev即可找到尾,比单链表中的找尾要方便许多。

插入newnode也非常简单,只需要改变四个指针的位置即可,以下是尾部插入的代码

void PushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* tail = phead->prev;LTNode* newnode = BuyLTNode(x);tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;
}

在对比一下以上两幅图和尾插的代码,首先写出一个开辟节点的代码用来开辟newnode(放在后面说)。

newnode已经开辟好,然后改变四个指针的方向;

先让哨兵位phead通过prev来找尾,令尾为tail;

让tail的next来指向新的结点newnode;

再让新结点newnode的prev链接上一个结点tail;

再让新节点newnode的next重新指向哨兵位;

最后让哨兵位phead的prev指向新节点newnode;这样newnode才能成为链表的尾结点;

这样就是一个完整的尾插;

兄弟萌,对比单链表的尾插,在逻辑上带头循环链表的尾插是要简单一些,一目了然。

2.2哨兵位的初始化

那为什么可以这么这么简单呢?

如下图

可以看到我们对哨兵位的初始化,如果链表为空时,phead的prev就指向自己,phead的next也可以指向自己,这就是对哨兵位的初始化

LTNode* LTInit()
{LTNode* phead = BuyLTNode(-1);phead->next = phead;phead->prev = phead;return phead;
}

2.3头部插入

既然都是插入我们不妨大胆猜测一下,他是否和尾插一样呢?

接下来继续画图分析:

头插要注意的是要插入到哨兵位之后,因为我们需要通过哨兵位来找到这个链表,所以要在哨兵位后面插入,也就是头插。

同样的上图先malloc了一个结点出来,那如何处理这个结点呢?

假设我们和尾插一样,先处理这个结点前面的指针

让phead的next指向newnode;

让newnode的prev指向phead;

再让newnode的next指向下一个结点;

这样做真的可以做到吗?

当然是不行的啦;

你有没有发现,如果我们先改变phead的next,令他指向newnode的话,它还可以找到newnode的下一个结点吗?显然是不行的,所以在头部插入时我们需要先改变newnode后面的结点,让newnode先和后面的结点链接起来,再让他和前面的结点进行连接,这样才是头插的正确用法。

下面是头插的代码

void PushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode= BuyLTNode(x);newnode->next = phead->next;phead->next->prev = newnode;phead->next = newnode;newnode->prev = phead;}

可以对照上图仔细阅读一下代码,应该不难看懂;

2.4 打印链表

既然我们上面讨论过头插和尾插,我们不妨将其输出验证一下结果

void LTPrint(LTNode* phead)
{assert(phead);printf("sentinel bit<==>");LTNode* cur = phead->next;while (cur != phead){printf("%d<==>", cur->Data);cur = cur->next;}printf("\n");
}

sentinel是哨兵位的意思,我们要通过哨兵位才能找到这些链表;

我们先定义一个新的结构体指针cur,让cur指向头节点的下一个结点,向后迭代,通过cur来遍历这个链表,简单来说就是从哨兵位后面的那个结点开始遍历,当这个结点知道下一个是哨兵位的时候结束了。

那结合上面的图示我们可以知道phead的next走到最后就会又回到phead的位置,所以我们不妨让cur=phead成为循环结束的标志,当cur!=phead 的时候就打印链表内容

我们应用上面的两个插入函数来验证

可以看到我们的头部插入和尾部插入,还有打印函数都非常的成功。

2.5尾部删除

我们不妨先看看单链表的尾部删除

可以看到步骤是相当的繁琐,因为不仅要找尾,还要判空,还要判断是否只有一个数据,非常非常的麻烦,可以说是集各种缺点于一身。

但是带头双向链表的尾部删除写起来非常爽

再继续画图来理解

带头双向链表的尾删只要通过phead的prev就可以找到尾结点tail,并且找到尾结点tail后可以继续通过tail->prev来找到tail的前一个结点,我们将他成为tailPrev

然后将phead的prev指向tailPrev这个结点;

再将tailPrev这个结点的next指向phead哨兵位,这样就把tail孤立出去了,此时tailPrev就成为了新的尾结点

最后再将tail用free释放掉就就可以达到尾删的结果

以下是代码

void PopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTNode* tail = phead->prev;LTNode* tailprev = tail->prev;free(tail);tailprev->next = phead;phead->prev = tailprev;
}

可以对照着上面的图和文字步骤仔细理解一下代码的内容,应该不难看懂。

2.6头部删除

既然是头部删除,那就继续要在哨兵位上动手脚,我们继续来画图理解

相信这个图也很清晰了,通过哨兵位phead找到下一个结点的next,也就是next的next,然后free掉next来达到删除的效果,再让原先next的next的prev来指向哨兵位,这样第二个结点就代替了第一个结点达到头部删除的效果,下面是代码

void PopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTNode* first = phead->next;LTNode* second = first->next;phead->next = second;second->prev = phead;free(first);}

我们不妨将phead->next定义为first,将next->next定义位second,这样代码的可读性就会大大提高,将代码对照以上文字描述和图片仔细理解,应该不难看懂。

当然在增强代码可读性方面还需要做的一点就是assert的断言;可以看到上面的代码中出现了

assert(!LTEmpty(phead));

这样一串代码中assert怎么断言一个函数呢?那这是什么意思呢?

我们用bool写一个函数

bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}

意思就是说如果链表中没有元素了,phead的next还是等于phead的话就说明链表已经空了,这样做的好处就是可以提醒你链表中已经没有元素来让你删除了,从而达到暴力检查让编译器报错的效果。

我们在主函数中使用以下我们上面所写的删除函数

可以看到我们的尾部删除已经删掉了尾部插入的4,那我们再多次使用删除函数会怎样

可以看到再main函数中直插入了四个数据,但是却使用了五次删除函数,运行时就会报错,而报错的内容就是我们刚刚写的布尔函数,它可以大大加强代码的可读性。

2.7查找结点

查找结点再带头双向循环列表中也不困难,同样的是要对链表进行遍历查找。我们要查找的是结点,所以定义函数类型的时候一定是结构体指针类型,找到了就返回他的结点,找不到就返回空,这就是大体思路

LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->Data == x){return cur;}cur = cur->next;}return NULL;}

这里就需要传两个参数了,一个是链表的头节点以便于我们找到链表并遍历查找,另一个就是我们想要找的数x。

当然也需要重新定一个结构体指针来遍历数组,并且和打印函数的循环条件相同,当检测到下一个结点时哨兵位phead的时候就停止循环了,因为下一个结点是phead的时候已经遍历完整个链表了。

这时就要操作结构体中的数据Data了,如果遍历时结点中的数据Data等于我们传入的参数x,那么就烦回这个结点,如果没有找到就返回空。

其实查找函数可以和其他的函数嵌套使用,因为查找函数返回的是结构体指针类型,而其它函数的参数也是结构体指针类型,我们可以将其和插入函数和删除函数一起使用。

2.8任意位置插入

和其他的插入方法一样的只需要改变指针的指向的内容即可。

以下是图例

void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* prev = pos->prev;LTNode* newnode = BuyLTNode(x);prev->next = newnode;newnode->prev = prev;newnode->next = pos;pos->prev = newnode;
}

中间插入就不需要再使用phead了,因为phead是哨兵位,而中间插入需要的是其他的结点,也就是通过刚刚讨论过的查找函数所查找出来的结点,你就会发现,查找函数和其他函数就这样连接在一起了。

首先定义一个结构体指针prev来存放查找出来的那个结点的前面一个结点;

然后开辟一个新的结点newnode;

然后就和尾插一模一样的方法改变指针指向的内容即可。

也可以在main函数中使用验证一下

这里的意思就是通过查找函数找到3的位置,并且再3的前面插入30,验证正确;

2.9任意位置删除

同样的也需要通过查找函数来确定删除的位置,以下是图例

仔细研究完头部删除和尾部删除的内容应该不难看懂。

首先要找到pos前面的一个结点和后面的一个结点,然后直接将前一个结点的next指向pos的下一个结点;

将pos的下一个结点的prev指向pos的上一个结点;

最后再free掉pos这个位置即可完成删除操作;

以下是任意位置删除的代码

void LTErase(LTNode* pos)
{assert(pos);LTNode* posPrev = pos->prev;LTNode* posNext = pos->next;posPrev->next = posNext;posNext->prev = posPrev;free(pos);;
}

继续再mian函数中使用

可以看到3就被删除掉了。

以上就是带头双向循环链表增删查改使用的所有内容,如果对你有所帮助还请多多三联支持,感谢您的阅读。

【数据结构】链表:带头双向循环链表的增删查改相关推荐

  1. 【数据结构】带头双向循环链表的增删查改(C语言实现)

    文章目录 前言 一.什么是带头双向循环链表 二.带头双向循环链表的实现 1.结构的定义 2.链表的初始化 3.开辟新节点 4.在头部插入数据 5.在尾部插入数据 6.查找数据 7.在pos位置之前插入 ...

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

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

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

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

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

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

  5. 【数据结构】带头+双向+循环链表的 增,删,查,改 的实现

    #include <iostream>using namespace std;int main() {typedef int ListType;typedef struct ListNod ...

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

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

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

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

  8. 数据结构:二叉搜索树的增删查改

    二叉搜索树的增删查改 二叉搜索树(Binary Search Tree) 基本操作之查找(Update) 基本操作之修改(Update) 基本操作之增加(Create) 基本操作之删除(Delete) ...

  9. 数据结构C语言实现顺序表——增删查改操作实现详解

    顺序表 顺序表是什么? 顺序表是将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示.实现增删查改的功能. 顺序表所需的头文件: #include<stdio.h> ...

最新文章

  1. 语义分割--Global Deconvolutional Networks for Semantic Segmentation
  2. URAL 1152. False Mirrors(DP)
  3. 【计算机网络】传输层 : 总结 ( TCP / UDP 协议 | 寻址与端口 | UDP 协议 | TCP 协议特点 | TCP 连接释放 | TCP 流量控制 | TCP 拥塞控制 ) ★★★
  4. 你当真了解count(*)count(id)count(1)吗?
  5. PowerDesigner的汉化破解安装到逆向工程(ORACLE)
  6. 数据分析CSV模块的基本使用(以分析复杂的天气情况为例),附完整的Python代码及csv文件详解---数据可视化
  7. vlookup使用步骤_使用vlookup出错,看看原因多为这几个!快来看看!
  8. 维珍银河创始人布兰森成功进入太空 早于贝佐斯9天
  9. 使用GDAL进行RPC坐标转换
  10. ubutntu 使用tftp_TI 816X开发板直接从SD卡读取内核到内存中和通过TFTP下载到内存中区别...
  11. 8大排序算法图文解说
  12. 计算机图形学课本pdf,计算机图形学教材.pdf
  13. 如何把二维表转成一维表
  14. 经济学中ppf计算机会成本例题,经济学中的PPF是什么
  15. git 记住账号密码、忽略部分文件、合并分支、将远程分支拉取到本地
  16. hadoop java 文件追加_HDFS追加文件
  17. 常见前端面试题及答案-转载
  18. 计算机绘图实训体会,CAD实习心得体会
  19. 再白也能学会的C-引子
  20. 叔本华《作为意志和表象的世界》

热门文章

  1. 富士康赋能,夏普电视上演王者归来
  2. 贸易相关术语[A-C]
  3. 机器学习 数据的采集和清洗
  4. BFS团战可以输、提莫必须死(转载)
  5. GD32f303 flash加密
  6. 为何会选择seo的方式优化
  7. 达梦数据库之巡检之道
  8. Python如何实现一步一步查看程序之代码调试-B04
  9. 李林蔚:打造全球第一商用公链
  10. css实现京东的价格标签