数据结构---手撕图解双向循环链表

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

写在前面

在前面学完单链表后,我们思考这样一个问题,单链表和顺序表比起来,功能确实相当强大,有很多优势,但是于此同时,我们也应思考下面的问题

单链表有什么不足的地方?

如果你把单链表的各个函数都自己实现过,那么下面的问题你一定有相同的感悟

  1. 单链表实现尾插尾删等操作,需要寻找尾部节点,而寻找尾部节点是一个相当繁琐的过程,需要从前向后寻找尾
  2. 单链表实现插入功能,对于单链表来说,通常实现的是在pos后插入,而不在pos前插入,原因就在于pos向前插入需要寻找前面的节点,而这个节点只能从前向后遍历寻找
  3. 单链表在实现函数时,要时时刻刻想到链表为空的情况,对于链表为空的情况要有特殊的处理方式

那么能不能想办法解决下面这些问题?

  1. 找尾部节点方便
  2. 找一个节点的上一个节点方便
  3. 不需要考虑空链表的空指针问题

于是数据结构中对于双向循环链表就解决了这个问题


双向循环链表的构造布局

带有哨兵位的布局

首先介绍什么是哨兵位

和它的名字一样,所谓哨兵位就是一个站哨的位置,它并不属于真实的队伍中的成员,而在链表中也是如此,在前面总结单链表所学的知识时,没有引入哨兵位的链表,而是直接用空链表进行写入,这样的目的不仅仅在于方便后续的OJ练习,同时也是适应特殊情况,为后续的c++学习做铺垫

而在双向循环链表中我们引入哨兵位的概念,作为哨兵的位置,它本身并不属于链表中的一部分,只是充当一个头的位置

哨兵位怎么设置?

链表的每一个节点我们都是通过结构体创建出来的,而很明显,哨兵位也是链表的节点,就意味着哨兵位也有指针部分和数据部分,那么对于数据部分我们应该怎么对它定义?

在一部分书中,哨兵位的数字部分会定义的链表的长度,也就是链表中元素的个数,这样乍一看似乎还不错,借助这个哨兵位还能获取到链表的长度

但是这样真的没问题吗?

事实上这是存在一定的问题的,假设这里选取的是char类型的数据类型,对于char类型的数据范围是从-128到127(char类型占1个字节决定的) 那么这里就有了一个新的问题,假设链表中存储的是char类型的数据,那么假设链表的长度为128呢?那么就会导致链表的实际长度和存储长度有很大差距

于是这里对于哨兵位我并不对它的数据部分有特殊的意义,单纯给它赋予一个值-1

带哨兵位的双向循环链表如下

数据结构---手撕图解双向循环链表,数据结构,知识总结,c语言,数据结构,链表,笔记
上面就是双向循环链表的示意图,从中可以看出,每一个节点可以轻松找到它的下一个节点,以及最后一个节点和头节点是循环在一起的

既然是双向的链表,那么在定义结构体的过程就和单链表有所不同,这里的指针部分应该有两个,prev和next部分,那么结构体的定义如下

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

这样就实现了对链表的定义,prev和next均有

和单链表相同,双向循环链表也离不开增删查改的基本操作,那么这里我们一个一个来实现这些功能

链表的构建

链表的构建就是构建一个phead的哨兵位节点,这个过程还是很简单的

ListNode* ListCreate()
{
	ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
	if (phead == NULL)
	{
		perror("malloc fail");
	}
	assert(phead);
	phead->_data = -1;
	phead->_next = phead;
	phead->_prev = phead;
	return phead;
}

链表的销毁

有创建就离不开销毁,销毁的过程和单链表基本相似,都是通过指针把一个节点单独拿出来,free后再置空,代码实现如下

void ListDestory(ListNode* pHead)
{
	assert(pHead);

	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		ListNode* next = cur->_next;
		free(cur);
		cur = next;
	}
}

链表的输出

链表要打印在屏幕上的基本思路也和单链表基本一致,但需要注意的是,单链表的截止条件是当指针访问到空,而双向循环链表的条件是指针访问到头

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	printf("guard<==>");
	while (cur != pHead)
	{
		printf("%d<==>", cur->_data);
		cur = cur->_next;
	}
	printf("\n");
}

这样,双向循环链表也可以进行可视化管理了,那么下面就开始实现增删查改四大功能

链表的尾插

和单链表相似,但和它比起来更加简单,示意图如下:

数据结构---手撕图解双向循环链表,数据结构,知识总结,c语言,数据结构,链表,笔记
那么代码是如何实现的?代码实现如下

ListNode* BuyListnode(LTDataType x)
{
	ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
	if (phead == NULL)
	{
		perror("malloc fail");
	}
	assert(phead);
	phead->_data = x;
	phead->_next = NULL;
	phead->_prev = NULL;
	return phead;
}

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* newnode = BuyListnode(x);
	ListNode* prevnode = pHead->_prev;

	prevnode->_next = newnode;
	newnode->_prev = prevnode;

	newnode->_next = pHead;
	pHead->_prev = newnode;
}

对比单链表的尾插不难发现,这个过程比单链表简单了很多,首先对于空链表的判断就更为简单,同时双向循环链表由于存在双向箭头,导致插入是很便利的,这个过程在pos位置插入时候会体现出双向链表特有的优势

链表的尾删

数据结构---手撕图解双向循环链表,数据结构,知识总结,c语言,数据结构,链表,笔记
代码实现如下

bool LTempty(ListNode* pHead)
{
	assert(pHead);
	return  pHead->_next == pHead;
}

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(!LTempty(pHead));

	ListNode* tail = pHead->_prev;

	pHead->_prev = tail->_prev;
	tail->_prev->_next = pHead;

	free(tail);
	tail = NULL;
}

== 这里我们对bool返回值的函数进行补充==

bool值意为真和假,在进行尾删时,我们需要首先进行判断链表到底是否为空,但由于双向循环链表,于是我们并不能直观通过判断空指针,这里封装了一个函数,用来判断是否为空,如果为空就返回真,如果不为空就返回假,那么我们在函数体内断言只需要看它不为空即可

链表的头插

数据结构---手撕图解双向循环链表,数据结构,知识总结,c语言,数据结构,链表,笔记
代码实现如下:

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* next = pHead->_next;
	ListNode* newnode = BuyListnode(x);
	assert(newnode);

	pHead->_next = newnode;
	newnode->_prev = pHead;

	next->_prev = newnode;
	newnode->_next = next;
}

链表的头删

从头删中其实就体现出了双向链表的优越性

数据结构---手撕图解双向循环链表,数据结构,知识总结,c语言,数据结构,链表,笔记

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(!LTempty(pHead));
	ListNode* first = pHead->_next;
	ListNode* second = first->_next;

	pHead->_next = second;
	second->_prev = pHead;

	free(first);
	first = NULL;
}

链表的查找

和单链表基本类似,这里不多介绍

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->_next;

	while (cur->_data != x)
	{
		cur = cur->_next;
	}

	return cur;
}

链表的插入

在pos前插入的原理如下

数据结构---手撕图解双向循环链表,数据结构,知识总结,c语言,数据结构,链表,笔记

从中和单链表一对比就能够体现出双向链表的优越的地方,在单链表中我们通常不在pos前插入,原因就是pos前面的节点并不方便寻找,而双向链表就解决了这个问题

代码实现如下

void ListInsert(ListNode* pos, LTDataType x)
{
	ListNode* posprev = pos->_prev;
	ListNode* newnode = BuyListnode(x);
	assert(newnode);
	assert(pos);

	posprev->_next = newnode;
	newnode->_prev = posprev;

	newnode->_next = pos;
	pos->_prev = newnode;
}

链表的删除

数据结构---手撕图解双向循环链表,数据结构,知识总结,c语言,数据结构,链表,笔记
这里和单链表的删除相似,不多描述

void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* posprev = pos->_prev;
	ListNode* posnext = pos->_next;

	posprev->_next = posnext;
	posnext->_prev = posprev;

	free(pos);
	pos = NULL;
}

自此,双向循环链表就实现完毕了,和单链表比起来,双向循环链表确实有它独特强大的地方,而真正使用什么数据结构还需要根据实际情况进行设计文章来源地址https://www.toymoban.com/news/detail-554276.html

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

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

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

相关文章

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

    在创建带头双向循环链表的节点中比之前单链表节点的创建多了一个struct ListNode* prev;结构体指针,目的在与存储前一个节点的地址,便于将整个链表连在一起。 动态申请内存结点,函数返回的是一个指针类型,用malloc开辟一个LTNode大小的空间,并用node指向这个空间,再判断

    2024年02月09日
    浏览(42)
  • 【数据结构】带头双向循环链表

      🧑‍🎓 个人主页:简 料   🏆 所属专栏:C++   🏆 个人社区:越努力越幸运社区   🏆 简       介: 简料简料,简单有料~在校大学生一枚,专注C/C++/GO的干货分享,立志成为您的好帮手 ~ C/C++学习路线 (点击解锁) ❤️ C语言阶段(已结束) ❤️ 数据结构与算法(ing) ❤

    2024年01月16日
    浏览(78)
  • 数据结构---带头双向循环链表

    什么是双向带头循环链表? 上面简单的一个非空 带头循环双向链表逻辑图 如何定义一个双向链表? 根据图和代码可以看双向链表就是单链表的每个结点中,在设置一个指向前驱节点的指针 简单认识之后,对他进行初始化(申请一个头节点,让前驱和后驱指针都指向自己) 代码

    2024年02月07日
    浏览(69)
  • 数据结构之带头双向循环链表

    目录 链表的分类 带头双向循环链表的实现 带头双向循环链表的结构 带头双向循环链表的结构示意图 空链表结构示意图 单结点链表结构示意图  多结点链表结构示意图 链表创建结点 双向链表初始化 销毁双向链表 打印双向链表  双向链表尾插 尾插函数测试 双向链表头插

    2024年02月08日
    浏览(73)
  • 数据结构_带头双向循环链表

    相较于之前的顺序表和单向链表,双向链表的逻辑结构稍微复杂一些,但是在实现各种接口的时候是很简单的。因为不用找尾,写起来会舒服一点。(也可能是因为最近一直在写这个的原因) 在实现接口的时候,除了没有找尾,其他的操作和单向链表是差不多的,这里就不多

    2024年04月14日
    浏览(63)
  • 数据结构篇三:双向循环链表

      前面我们学习了单链表的实现,我们发现它在进行从后往前查找的时候有很大的不便,为了解决这个问题,双向链表油然而生。它可以很好的解决单链表无法从后往前查找的困难。   如图所示,它是有两个指针域,一个指向后一个结点,一个指向前一个结点。它存储了

    2024年02月02日
    浏览(37)
  • 【数据结构】实现带头双向循环链表

    之前我们已经学习了单链表,有了单链表的基础,现在开始学习带头双向循环链表~ 结构最复杂 ,一般用在单独存储数据。 实际中使用的链表数据结构,都是带头双向循环链表 。另外这个结构虽然结构复杂,但是使用代码实现以后会发现 结构会带来很多优势 ,实现反而简单

    2024年02月10日
    浏览(45)
  • 【数据结构】线性表——带头双向循环链表

    带头双向循环链表的优点 1.支持任意位置时间复杂度为O(1)的插入和删除。 2.按照需求申请释放空间,无需担心空间不够用,无需担心浪费。 3.带头可以省去链表为空时的判断,可以使代码更加简约 带头双向循环链表的缺点 1.不可以进行下标随机访问。 2.缓存利用率低 带头双

    2024年02月03日
    浏览(69)
  • 【数据结构】带头双向循环链表及其实现

    目录 1.带头双向循环链表 2.带头双向循环链表实现 2.1初始化 2.2销毁 2.3头插 2.4链表打印 2.5头删数据 2.6尾插数据 2.7尾删数据 2.8链表判空  2.9查找一个数据 2.10在pos位置前插入数据 2.11删除pos位置 2.12求链表的长度 2.顺序表和链表的比较 我们已经实现了无头单向循环链表 带头双

    2024年02月10日
    浏览(45)
  • 数据结构入门指南:带头双向循环链表

    目录 文章目录 前言 1.结构与优势 2.链表实现       2.1 定义链表 2.2 创建头节点 2.3 尾插 2.4 输出链表 2.5 尾删 2.6 头插 2.7头删 2.8 节点个数 2.9 查找 2.10 位置插入 2.11 位置删除 2.12 销毁链表  3. 源码 总结         链表一共有8种结构,但最常用的就是无头单向链表、和带头

    2024年02月14日
    浏览(51)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包