数据结构与算法:双向链表

朋友们大家好啊,在上节完成单链表的讲解后,我们本篇文章来对带头循环双向链表进行讲解

双向链表

  • 双向链表、头节点和循环的介绍
  • 构建双向链表
    • 节点的构建
    • 初始化双向循环链表(空链表)
    • 销毁双向链表
  • 链表的打印
  • 双向链表头尾的插与删
    • 尾插
    • 尾删
    • 头插
    • 头删
  • 查找特定节点
    • 在指定位置前插入数据
    • 删除pos节点
  • 总结

双向链表、头节点和循环的介绍

在这里插入图片描述
单链表中,一个节点存储数据和指向下一个节点的指针,而双向链表除了上述两个内容,还包括了指向上一个节点的指针
在这里插入图片描述

带头的双向链表,是指在双向链表的最前端添加了一个额外的节点,这个节点被称为头节点(哨兵节点),但它一般不用于存储实际的数据(或者可以说存储的数据不被使用)。头节点的主要目的是为了简化链表操作的逻辑,避免在处理链表的开始和结束位置时需要进行特殊的条件判断

在没有头节点的普通双向链表中,如果链表为空,则链表的第一个节点(head pointer)直接为NULL,这使得插入和删除操作时,需要分别检查特定情况,如链表是否为空、是否在链表开始或结束位置进行操作等。

循环链表,即最后一个节点指向下一个节点的指针并不指向空,而是指向头结点,且头结点的指向上一个节点的指针也并不指向空,而是指向最后一个节点

简单介绍之后,我们就来讲解双向循环链表的各个细节吧

构建双向链表

typedef int LTDatatype;typedef struct ListNode
{struct ListNode* next;struct ListNode* prev;LTDatatype val;
}LTNode;

这里typedef int LTDatatype;我们多次提到,为类型抽象

构建的节点中,每个节点包括两个指针:

  • struct ListNode* next;
    这是一个指针,指向下一个ListNode节点。在链表中,每个节点通过这样的next指针连接到下一个节点。对于链表的最后一个节点,这个指针通常设为NULL,表示没有后续节点。但在循环链表的情况下,最后一个节点的next指针会指向链表的第一个节点,形成一个闭环。

  • struct ListNode* prev;
    这是另一个指针,指向前一个ListNode节点。在双向链表中,除了能够向前遍历,我们还可以通过这个prev指针向后遍历链表。对于链表的第一个节点,这个指针在非循环链表中通常设为NULL,表示没有前驱节点**。而在循环链表中,第一个节点的prev指针会指向链表的最后一个节点。**

节点的构建

我们首先定义一个函数

LTNode* CreatNode(LTDatatype x)

与单链表不同的是,这个函数多了一个指向前一个节点的指针,其他内容均相同

LTNode* CreatNode(LTDatatype x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");exit(-1);}newnode->val = x;newnode->next = NULL;newnode->prev = NULL;return newnode;
}

初始化双向循环链表(空链表)

在双向循环链表中,空链表的标志性质是其头节点的 next 和 prev指针都指向它自身。即使是空的链表,依然保持着循环的特性,但它不包含任何数据节点,只有这一个特殊的头节点

这里有两种初始化的形式

void LTInit(LTNode** phead)
{*phead = (LTNode*)malloc(sizeof(LTNode)); if (*phead != NULL) {(*phead)->next = *phead;(*phead)->prev = *phead; }
}

phead 代表指向链表的“头节点”的指针

在这个初始化函数中,新创建的链表头节点的 next 和 prev 指针都被设置为指向自身,形成一个空的双向循环链表,这里用了二级指针,是因为我们对phead进行了改变,对指针进行改变,则需要二级指针
这种方法我们初始化格式如下,首先创造一个plist结构体指针,再传参

LTNode* plist;
LTInit(&plist);
LTNode* LTInit2() {LTNode* phead = (LTNode*)malloc(sizeof(LTNode));if (phead != NULL) {phead->prev = phead;phead->next = phead;}return phead; 
}

在这个实现中,LTInit函数不接受任何参数,而是直接创建并初始化一个新的头节点,使其prev和next指针都指向自己,从而形成一个空的双向循环链表。这样设计的好处是简化了链表的初始化过程,你只需要调用LTInit来获取一个新的链表头节点即可
这种方法我们直接用plist接收返回值即可

LTNode* plist=LITnit2();

销毁双向链表

void ListDestroy(LTNode* phead) {if (phead == NULL) {return;}// 由于是循环链表,我们需要一个指针指向第一个节点LTNode* current = phead->next;// 如果链表不只是头节点自己循环(即有实际数据节点)if (current != phead) {do {LTNode* temp = current;current = current->next; // 移动到下一个节点free(temp); // 释放当前节点内存} while (current != phead);}// 最后,释放头节点内存(如果头节点是哨兵节点并且是动态分配的)free(phead);
}

函数首先检查传入的链表是否为空。如果不为空,它会进入一个 do-while 循环,这个循环确保至少运行一次,即使链表中只有一个节点(头节点)
在循环内部,它会释放当前节点的内存,并移动到下一个节点,直到它循环回到头节点。最后,它释放头节点的内存

链表的打印

在单链表中,我们进行循环打印的判断条件是最后一个节点的指针是否指向NULL,而在双向循环链表中,没有空指针,我们的判断条件也有所不同

void LTPrint(LTNode* phead) {if (phead == NULL || phead->next == phead) {return;}LTNode* current = phead->next;while (current != phead) { printf("%d ", current->val); current = current->next; }printf("\n"); 
}

首先

if (phead == NULL || phead->next == phead) {return;
}

这串代码是判断链表是否为空或者链表是否只有一个头结点,如果是,则没有数据可打印,直接返回

遍历链表:

LTNode* current = phead->next;
while (current != phead) { printf("%d ", current->val); current = current->next; 
}

这部分代码初始化一个新指针 current 指向链表的第一个节点(即 phead->next),然后进入一个 while 循环。在循环中,只要 current 不指回 phead,它就打印当前节点的值,并移动到下一个节点。这个循环确保了所有节点都被访问一次。

注意,由于它从 phead->next 开始,phead 本身不存储有效数据(或者说是一个哨兵节点)

双向链表头尾的插与删

尾插

void LTPushBack(LTNode* phead, LTDatatype x) {LTNode* newnode = CreatNode(x);if (phead == NULL) {return;}newnode->next = phead; newnode->prev = phead->prev;phead->prev->next = newnode;phead->prev = newnode;
}

在这里插入图片描述
我们构建newnode

  • newnode的next指向头结点newnode->next = phead;
  • 原来的phead的prev指针指向倒数第二个节点,那么newnode的前一个指针则为初始时phead的prev指针newnode->prev = phead->prev;
  • 现在更新倒数第二个节点的下一个指针,原来指向头指针,现在指向newnode:phead->prev->next = newnode;
  • 最后更改phead的prev指针,指向尾部的newnodephead->prev = newnode;

测试代码如下:
在这里插入图片描述

尾删

void LTPopBack(LTNode* phead) {if (phead == NULL || phead->next == phead) {return;}LTNode* tail = phead->prev; LTNode* tailprev = tail->prev; // 断开当前末尾节点与链表的连接,形成新的末尾tailprev->next = phead;phead->prev = tailprev;// 释放原末尾节点占用的内存free(tail);
}
  • 首先判断是否为空链表或者只有哨兵节点,如果是则没有值可以删除,直接返回
  • 找到尾部节点tail,即头结点的前一个指针指向的节点;
  • 再找到tail前面的节点,即预期的尾节点将这个节点的下一个指针指向头结点,并将头节点的前一个指针指向这个节点
  • 将tail这个尾部节点内存释放

测试代码如下:
在这里插入图片描述

头插

void LTPushFront(LTNode* phead, LTDatatype x) {LTNode* newnode = CreatNode(x); if (phead == NULL) {return;}newnode->next = phead->next; newnode->prev = phead;       phead->next->prev = newnode; phead->next = newnode;       
}
  • 首先判断链表是否为空,为空直接返回
  • 新节点的next指针指向原来头节点的下一个节点:newnode->next = phead->next;
  • 新节点的prev指针指向头结点:newnode->prev = phead;
  • 接着更新头节点之后的节点的prev指针,以及头节点的next指针
    - 原来头节点之后的节点的prev指针现在应该指向新节点:phead->next->prev = newnode;
    - 头节点的next指针现在应该指向新节点:phead->next = newnode;

我们更新了四个指针:新节点的前后指针,头结点的next指针,后一个节点的prev指针

测试代码:
在这里插入图片描述

头删

void LTPopFront(LTNode* phead) {if (phead == NULL || phead->next == phead) {return;}LTNode* first = phead->next;phead->next = first->next;first->next->prev = phead;free(first);
}
  • 首先检查链表是否为空或者只有哨兵节点
  • 找到要删除的节点,它是头节点的下一个节点:LTNode* first = phead->next;
  • 更新头节点的next指向被删除节点的下一个节点:phead->next = first->next;
  • 更新新的第一个有效数据节点的prev指向头节点:first->next->prev = phead;
  • 最后释放被删除节点所占用的内存

测试代码:
在这里插入图片描述

查找特定节点

LTNode* ListFind(LTNode* phead, int x) {if (phead == NULL || phead->next == phead) {return NULL;}LTNode* current = phead->next; while (current != phead) { if (current->val == x) {return current;}current = current->next; }return NULL;
}
  • 如果链表为空或者只有哨兵节点,直接返回
  • 由于第一个节点没有有效数据,我们可以从 phead 的下一个节点开始遍历
  • 在这个实现中,我们从哨兵节点的下一个节点开始遍历,即从链表的第一个实际数据节点开始。循环继续执行,直到 current 指针再次回到哨兵节点 phead。如果找到一个节点的值与 x 相等,函数返回该节点的指针。如果遍历完所有节点都没有找到,则返回 NULL。

在指定位置前插入数据

void ListInsert(LTNode* pos, LTDatatype x)
{if (pos == NULL){return;}LTNode* posprev = pos->prev;LTNode* newnode = CreatNode(x);posprev->next = newnode;newnode->prev = posprev;newnode->next = pos;pos->prev = newnode;
}
  1. 找到pos前面的节点posprev
  2. 构建新节点
  3. posprev的next指针指向newnode;
  4. newnode的prev指针指向posprev,next指针指向pos
  5. pos的前一个指针指向newnode;

测试代码,在1 2 3 4 5的3前面插入8,首先获得3节点的地址,在传入插入函数中

在这里插入图片描述
如果再哨兵节点位置,往前插入,则相当于尾插

删除pos节点

我们假设pos不为哨兵节点

void ListErase(LTNode* pos) {if (pos == NULL) {return;}pos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);
}

这个代码就非常简单了,改变指针后将空间释放

测试代码,删除1 2 3 4 5中的3
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/b7333188533e4900a9d0d59b6234dd66.png
这里注意置空temp

总结

对比于顺序表,双向带头循环链表有以下优势:

  • 在任意位置添加或删除元素的时间复杂度都是O(1)
  • 按需要进行申请空间,没有浪费

不足之处

  • 下标随机访问不方便,需要遍历链表,时间复杂度为O(N);

顺序表和双向带头链表根据特定的使用场景和需求具有各自的优势和劣势。选择哪种数据结构,取决于对性能、内存使用、以及操作灵活性的具体要求。

本节内容到此结束,感谢大家的阅读!!!

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

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

相关文章

2024】前端,该卷什么呢?_2024-02-16

2024已来,过去的 2023 可以说是具有里程碑意义的一年,ChatGPT 的炸裂式发展,很多大佬都亲自入场整活儿,你不得不说,人工智能时代的未来已来,大势所趋,不可阻挡。随着生成式AI的迅猛发展&#xf…

AI:128-基于机器学习的建筑物能源消耗预测

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的关键代码,详细讲解供…

怎么恢复电脑重装前的数据?介绍几种有效的方法

在日常生活和工作中,电脑已成为我们不可或缺的工具。然而,有时候我们会遇到一些突发情况,比如电脑系统崩溃需要重新安装系统。在这个过程中,我们可能会失去一些重要的数据,比如照片、文档、视频等。这些数据可能包含着…

【NLP】MHA、MQA、GQA机制的区别

Note LLama2的注意力机制使用了GQA。三种机制的图如下: MHA机制(Multi-head Attention) MHA(Multi-head Attention)是标准的多头注意力机制,包含h个Query、Key 和 Value 矩阵。所有注意力头的 Key 和 V…

BBC英式口语~发音练习~笔记整理

参考资料 原视频地址: https://www.bilibili.com/video/BV1D7411n7bS/?spm_id_from333.1245.0.0&vd_source5986fc7c8e6d754f3ca44233573aeaff 笔记图片

Java Web 中forward 和 redirect 的区别

前言 在Java Web开发中,页面跳转是构建用户界面和实现业务逻辑的重要组成部分。Forward(转发)和Redirect(重定向)是两种常见的跳转方式,它们分别具有不同的特点和适用场景。正确地选择和使用这两种跳转方式…

html的列表标签

列表标签 列表在html里面经常会用到的,主要使用来布局的,使其整齐好看. 无序列表 无序列表[重要]: ul ,li 示例代码1: 对应的效果: 无序列表的属性 属性值描述typedisc,square,…

文生视频:Sora模型报告总结

作为世界模拟器的视频生成模型 我们探索视频数据生成模型的大规模训练。具体来说,我们在可变持续时间、分辨率和宽高比的视频和图像上联合训练文本条件扩散模型。我们利用对视频和图像潜在代码的时空补丁进行操作的变压器架构。我们最大的模型 Sora 能够生成一分钟…

【硬核】堪称全地球最好用并且免费的AI人工智能图片无损放大神器,我只偷偷告诉你一个人!

🚀 个人主页 极客小俊 ✍🏻 作者简介:web开发者、设计师、技术分享博主 🐋 希望大家多多支持一下, 我们一起学习和进步!😄 🏅 如果文章对你有帮助的话,欢迎评论 💬点赞&a…

C语言—函数

1.编写一个函数&#xff0c;通过输入一个数字字符&#xff0c;返回该数字29. /*1.编写一个函数&#xff0c;通过输入一个数字字符&#xff0c;返回该数字 */#include <stdio.h>//函数定义,返回类型为int int char_num(char c) {if(c > 0 && c < 9) //检查…

Java基于 SpringBoot 的高校校园点餐系统,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

C++中对象的构造与析构顺序

一、对象的构造顺序 对象的构造&#xff0c;先被创建的对象&#xff0c;先被构造&#xff0c;先调用其构造函数 class A { private:int _a 0; public://构造函数A(int a 0){_a a;cout << "A(int a 0)" << " " << _a << endl…