【数据结构】从头到尾全解析双向链表

这篇具有很好参考价值的文章主要介绍了【数据结构】从头到尾全解析双向链表。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

在之前我们已经讲过< 单链表 >了,单链表查找上一个结点的时间复杂度为O(n),尾插时也要遍历一次链表也是O(n),因为我们每次都要从头开始遍历找,为了克服这单向性的缺点,我们就有了双向链表.
如果要提高链表的查找,尾插等效率,那双向链表(双链表)无疑是首选。

双向链表的概念及结构

双向链表是一种常用的数据结构,它允许我们在O(1)时间内对链表的头尾进行元素的添加和删除操作,同时也支持双向遍历。

双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域,所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。
【数据结构】从头到尾全解析双向链表

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


双向链表的结构:双向链表的每个节点包含了三个基本元素,分别是元素值、指向前一个节点的指针和指向下一个节点的指针。

  • 双向链表存储结构
struct SListNode
{
	int data;  //节点存储数据
	struct SListNode* next; //指向前驱节点
	struct SListNode* prev; //指向后驱节点
};
  • 注意:

双向链表头指针是一个虚拟头节点,不存储任何有效数据,他的前驱节点与后驱节点都是指向自己的,头指针节点相当于是一个削兵。用来站岗的。方便链接前后指针.


双向链表接口的实现

申请节点空间

  • 链表添加一个节点数据时候,每次都要写一段代码,这样做是不是太繁琐了,我们可以封装一个函数来解决问题,每次添加一个节点时,将结点存放的数据置为需要存放的值,在将结构体存放节点的地址置为NULL, 需要增加节点时调用一下改函数即可。
//申请节点空间
LTNode* BuyList(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)  //判断是否申请成功
	{
		perror("malloc fail");
	}
	else   //申请成功
	{
		newnode->data = x;  //将结点存放的数据置为需要存放的值
		newnode->next = NULL;  //节点的地址置为NULL
		newnode->prev = NULL;  //节点的地址置为NULL
		return newnode;
	}
}

双向链表的初始化

  • 其实在双向链表操作中,我们只需要一级指针接收就能修改链表的指向了,但在初始化时候我们要修改头节点指针,需要二级指针来接收修改头节点,为了后面统一用一级指针,所以我们在初始化时候不传参,直接申请一个节点,然后返回,该指点的前驱指针与后驱指针都是指向自己。节点数据不存储有效数据
LTNode* LTInit() 
{
	LTNode* phead = BuyLTNode(-11111);//节点数据不存储有效数据
	//前驱指针与后驱指针都是指向自己
	phead->next = phead;  
	phead->prev = phead;
	return phead; //返回该节点
}

双向链表打印数据

  • 双向链表是通过结构体存储的头指针下一个指针开始遍历链表的,当遍历的指针指向头指针则遍历完毕。
void LTPrint(LTNode* phead)
{
	assert(phead); //断言:判断是否为空

	printf("guard<==>");
	LTNode* cur = phead->next; //从头指针的next开始遍历
	while (cur != phead)
	{
		printf("%d<==>", cur->data); // 打印数据
		cur = cur->next; //迭代指向下一个
	}
	printf("\n");
}

双向链表是否为空

  • 如果phead->next为自己,则链表为空,返回真,反之返回假.
bool LTEmpty(LTNode* phead)
{
	assert(phead); //断言 判断是否为空指针

	return phead->next == phead; 
}

双向链表尾插

  • 双向链表尾插步骤
    1. 创建一个新节点 ,将新节点的数据填充为要插入的数据。
    2. 使用phead->prev找到当前双向链表的尾节点tail。
    3. 将tail节点的next指针指向新的待插入节点newnode
    4. 将待插入节点newnode的prev指针指向原尾节点tail
    5. 将待插入节点newnode的next指针指向链表的头节点phead。
    6. 将链表头节点phead的prev指针指向插入的新节点newnode。
// 双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead); //断言:判断是否为空
	LTNode* tail = phead->prev;  //使用phead->prev找到当前双向链表的尾节点tail。
	LTNode* newnode = BuyLTNode(x); //创建一个新节点 

	tail->next = newnode;  //将tail节点的next指针指向新的待插入节点newnode
	newnode->prev = tail;  //将待插入节点newnode的prev指针指向原尾节点tail
	newnode->next = phead; //将待插入节点newnode的next指针指向链表的头节点phead。
	phead->prev = newnode; //将链表头节点phead的prev指针指向插入的新节点newnode。
}

双向链表头插

  • 双向链表头插步骤
    1. 创建一个新节点 ,将新节点的数据填充为要插入的数据。
    2. 使用phead->next找到当前双向链表的头节点,用first保存.
    3. 将链表头节点phead的next指针指向新插入的节点newnode
    4. 将待插入节点newnode的prev指针指向链表的头节点phead。
    5. 将待插入节点newnode的next指针指向原头节点first。
    6. 将原头节点first的prev指针指向插入的新节点newnode。
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead); //断言 判断是否为空
	LTNode* newnode = BuyLTNode(x); //创建一个新节点 ,将新节点的数据填充为要插入的数据
	LTNode* first = phead->next; //使用phead->next找到当前双向链表的头节点

	phead->next = newnode;  //将链表头节点phead的next指针指向新插入的节点newnode
	newnode->prev = phead;  //将待插入节点newnode的prev指针指向链表的头节点phead。

	newnode->next = first;  //将待插入节点newnode的next指针指向原头节点first。
	first->prev = newnode;  //将原头节点first的prev指针指向插入的新节点newnode。
}

双向链表尾删

  • 双向链表尾删步骤
    1. 判断链表是否为空,如果为空,则无法进行删除操作
    2. 使用phead->prev找到当前双向链表的尾节点.(用一个临时变量tail来记录)
    3. 使用tail->prev找到当前尾节点的前一个节点。(用一个临时变量tailPrev来记录)
    4. 释放尾节点tail的内存空间
    5. 将tailPrev的next指针指向链表头节点phead。
    6. 将链表头节点phead的prev指向tailPrev
void LTPopBack(LTNode* phead)
{
	assert(phead);  //判断头指针是否为空
	assert(!LTEmpty(phead)); //断言:判断链表是否为空,如果为空,则无法进行删除操作
	LTNode* tail = phead->prev;    //使用phead->prev找到当前双向链表的尾节点.
	LTNode* tailPrev = tail->prev; //使用tail->prev找到当前尾节点的前一个节点

	free(tail); //释放尾节点tail的内存空间
	tailPrev->next = phead; //将tailPrev的next指针指向链表头节点phead
	phead->prev = tailPrev; //将链表头节点phead的prev指向tailPrev

}

双向链表头删

  • 双向链表头删步骤
    1. 判断链表是否为空,如果为空,则无法进行删除操作
    2. 使用phead->next找到当前双向链表的头节点下一个。(用一个临时变量first来记录)
    3. 使用first->next找到当前头节点的下一个节点(用一个临时变量second来记录)
    4. 将链表头节点phead的next指针指向second节点。
    5. 将second节点的prev指针指向链表头节点phead。
    6. 释放头节点first的内存空间,进行头删.
void LTPopFront(LTNode* phead)
{
	assert(phead); 判断头指针是否为空
	assert(!LTEmpty(phead)); 断言:判断链表是否为空,如果为空,则无法进行删除操作

	LTNode* first = phead->next;  //使用phead->next找到当前双向链表的头节点的下一个
	LTNode* second = first->next; //使用first->next找到当前头节点的下一个节点

	phead->next = second;  //将链表头节点phead的next指针指向second节点
	second->prev = phead;  //将second节点的prev指针指向链表头节点phead

	free(first); //进行头删
}

双向链表的查找

  • 获得链表某个节点的数据思路也较简单
    1. 从头节点的下一个节点开始,依次遍历链表中的每个节点。
    2. 在每个节点的数据域中查找节点的值是否为x,如果是则返回该节点的指针
    3. 如果遍历完整个链表都没有找到节点的值为x,则返回NULL。
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead); ///判断头指针是否为空

	LTNode* cur = phead->next; //从头节点的下一个节点开始,依次遍历链表中的每个节点。
	while (cur != phead)
	{
		if (cur->data == x) //在每个节点的数据域中查找节点的值是否为x,如果是则返回该节点的指针
		{
			return cur;
		}

		cur = cur->next; //迭代 指向下一个
	}

	return NULL; //如果遍历完整个链表都没有找到节点的值为x,则返回NULL。
}

双向链表在指定位置前插入数据

  • 双向链表插入数据不用像单链表一样从头查找,双向链表只需原地动数据就可。具体步骤如下:
    1. 获取pos节点的前一个节点(使用一个临时变量tmp来保存)
    2. 创建一个新的待插入节点newnode
    3. 将tmp节点的next指向待插入节点newnode
    4. 将待插入节点newnode的prev指针指向tmp节点
    5. 将待插入节点newnode的next指针指向pos节点
    6. 将pos节点的prev指针指向待插入节点newnode
// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//节点pos不为空

	LTNode* tmp = pos->prev; //获取pos节点的前一个节点
	LTNode* newnode = BuyLTNode(x); //创建一个新的待插入节点newnode

	tmp->next = newnode; //将tmp节点的next指向待插入节点newnode
	newnode->prev = tmp; //将待插入节点newnode的prev指针指向tmp节点
	newnode->next = pos;  //将待插入节点newnode的next指针指向pos节点
	pos->prev = newnode;  //将pos节点的prev指针指向待插入节点newnode
}

双向链表删除指定位置的值

  • 删除指定位置的具体逻辑步骤
    1. 获取pos节点的前一个节点和后一个节点。(用posprev和posnext临时变量来记录)
    2. 将posPrev节点的next指向posNext节点
    3. 将posNext节点的prev指向posPrev节点
    4. 释放要删除的节点pos的内存空间。
void LTErase(LTNode* pos)
{
	assert(pos); //节点pos不为空
	
    //获取pos节点的前一个节点和后一个节点
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

	posPrev->next = posNext;  //将posPrev节点的next指向posNext节点
	posNext->prev = posPrev;  //将posNext节点的prev指向posPrev节点
	free(pos);  //释放要删除的节点pos的内存空间。
}

双向链表的销毁

  • 当我们不打算使用这个单链表时,我们需要把它销毁,其实也就是在内存中将它释放掉,以便于留出空间给其他程序或软件使用。释放内存具体步骤如下:
    1. 使用一个指针变量遍历整个链表在循环中,对于当前遍历到的节点,将其保留下一个节点的指针,以便于在释放当前指针后,指向的节点时能够顺利找到下一个节点.
    2. 先从头指针的下一个节点开始释放,最后在释放头指针.
void LTDestroy(LTNode* phead)
{
	assert(phead); //断言:头指针不能为空

	LTNode* cur = phead->next;//先指向有效位置头指针的下一个节点
	while (cur != phead)
	{
		LTNode* next = cur->next; //保存下一个指针
		free(cur); //释放cur节点占用的内存空间,
		cur = next; //迭代
	}

	
	free(phead); //释放头指针
}

总结

  • 双向链表相对于单向链表的优点在于它可以支持双向遍历,即从链表头或尾部开始遍历,因此它可以在某些情况下更加高效。同时,双向链表也支持在任意位置进行链表节点的插入和删除操作,这是单向链表不支持的。因此,双向链表更加灵活,可以满足更多场景下的需求。
  • 相对于单链表,双向链表的节点需要额外存储一个指针,即指向前面节点的指针,因此相对于单链表会占用更多的内存空间。

总之,双向链表是一种值得掌握的重要数据结构,掌握了它的基本操作和应用场景,可以大大提高算法和数据结构等领域的编程水平。文章来源地址https://www.toymoban.com/news/detail-475279.html

到了这里,关于【数据结构】从头到尾全解析双向链表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Vue详解----一篇带你从头领悟到尾,享受飞升的感觉

    \\\"\\\"\\\" \\\"\\\"\\\" vue.js与vue.runtime.xxx.js的区别: vue.js是完整版的Vue,包含:核心功能 + 模板解析器。 vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。 因为vue.runtime.xxx.js没有模板解析器,所以不能使用template这个配置项,需要使用render函数接收到的createElement函数去指

    2024年02月16日
    浏览(29)
  • Servlet:实现动态页面的技术,看我从头到尾带你穿过servlet基础的海洋,使君有所获,有所悟

    什么是servlet ? Servlet 是一种实现动态页面的技术 . 换句话说,servlet是 java为实现web开发 提供的一些规范,而想要去参与web开发的一些软件,就要满足servlet的规范,而 这些满足servlet规范的软件我们称其为web开发的中间件(tomcat是一个常用的servlet容器) ,tomcat就是其中比较

    2024年02月06日
    浏览(35)
  • 计算机网络基础知识(五)——什么是TCPUDP协议?图文并茂的方式对两大传输层协议进行从头到尾的讲解

    TCP和UDP协议是TCP/IP协议的核心。 TCP 传输协议:TCP 协议是一TCP (Transmission Control Protocol)和UDP(User Datagram Protocol)协议属于传输层协议。其中TCP提供IP环境下的数据可靠传输,它提供的服务包括数据流传送、可靠性、有效流控、全双工操作和多路复用。通过面向连接、端到端和可靠

    2024年02月05日
    浏览(43)
  • 【几乎最全/全网最长的 2 万 字】前端工程化完整流程:从头搭到尾(vue3 + vite + qiankun + docker + tailwindcss + iview......)

    使用 Vite + Vue3 + Typescript + axios + echarts + pinia + view-ui-plus + vue-router + less + sass + scss + css + tailwindcss + animate.css + vite-svg-loader + postcss + stylelint + eslint + prettier + autoprefixer + commitizen + commitlint + vite-plugin-compression + vite-plugin-qiankun + Docker + nginx.conf…等等插件,带你从 0 开始一步一步搭

    2024年02月12日
    浏览(51)
  • 数据结构之双向带头循环链表函数功能实现与详细解析

    个人主页:点我进入主页 专栏分类:C语言初阶      C语言程序设计————KTV       C语言小游戏     C语言进阶 C语言刷题       数据结构初阶 欢迎大家点赞,评论,收藏。 一起努力,一起奔赴大厂。 目录 1.前言 2.带头双向循环链表函数实现 3.总结         在前面我

    2024年02月05日
    浏览(42)
  • 从头开始:数据结构和算法入门(时间复杂度、空间复杂度)

        目录 文章目录 前言 1.算法效率 1.1 如何衡量一个算法的好坏 1.2 算法的复杂度 2.时间复杂度  2.1 时间复杂度的概念 2.2 大O的渐进表示法 2.3常见时间复杂度计算 3.空间复杂度 4.常见复杂度对比 总结 前言         C语言的学习篇已经结束,今天开启新的篇章——数据结构

    2024年02月14日
    浏览(39)
  • 【数据结构】双向奔赴的爱恋 --- 双向链表

    关注小庄 顿顿解馋๑ᵒᯅᵒ๑ 引言:上回我们讲解了单链表(单向不循环不带头链表),我们可以发现他是存在一定缺陷的,比如尾删的时候需要遍历一遍链表,这会大大降低我们的性能,再比如对于链表中的一个结点我们是无法直接访问它的上一个结点,那有什么解决方法呢

    2024年04月08日
    浏览(84)
  • 数据结构-链表结构-双向链表

    双向链表也叫双链表,与单向链表不同的是,每一个节点有三个区域组成:两个指针域,一个数据域 前一个指针域:存储前驱节点的内存地址 后一个指针域:存储后继节点的内存地址 数据域:存储节点数据 以下就是双向链表的最基本单位 节点的前指针域指向前驱,后指针

    2024年02月04日
    浏览(36)
  • 数据结构——双向链表

    🍇系列专栏:🌙数据结构 🍉  欢迎关注:👍点赞🍃收藏🔥留言 🍎 博客主页:🌙_麦麦_的博客_CSDN博客-领域博主 🌙如果我们都不能够拥有黑夜,又该怎样去仰望星空?   目录 一、前言 二、正文——双向链表的实现 2.1模块化 2.2 数据类型与结构体定义  2.3链表的初始化

    2024年02月02日
    浏览(35)
  • 数据结构-双向链表

    在单链表那一篇博客中介绍了单链表和双向链表的优缺点,所以此篇博客直接分享怎样实现一个带头双向循环链表。 单链表博客: 首先我们需要写一个结构体,双向带头链表的话需要一个前驱指针prev和一个后驱指针next,前驱指针的作用是方便找尾节点,因为头节点的prev指

    2024年02月05日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包