【大型数据结构连续剧之----带头双向循环链表】带你通透这傲娇的结构

前言


  • 🚄 输入是学习的本质,输出是学习的手段。
  • 🔖 分享每一次的学习,期待你我都有收获。
  • 🎇 欢迎🔎关注👍点赞⭐️收藏✉评论,共同进步!
  • 🌊 “要足够优秀,才能接住上天给的惊喜和机会”
  • 💬 博主水平有限,如若有误,请多指正,万分感谢!

在这里插入图片描述为什么说它傲娇呢,因为它在逻辑结构中表现得很复杂,让人不敢靠近。 但其实真正写起代码来,对大家伙子们可以说是百般照顾!那叫一个顺畅


一、带头双向循环链表的结构优势

当我第一次看到双向循环链表的时候,也不禁皱了皱眉头,相比于无头单向非循环链表,它的逻辑结构看上去似乎更加复杂,但其实,这也正是带头双向循环链表的结构优势:

它的每一个节点之间都是存在相互联系的

当我们需要进行插入、删除、移动的操作时,我们不需要像单向链表那样,从第一个节点开始遍历直到我们要的前驱节点。只要调用指向前驱的指针,就能找到我们要的前驱节点了。

因此,带头双向循环链表其实是一个没有缝隙的数据结构,在实现代码时,我们不需要像构造单链表g各个接口函数时,谨慎地去考虑头节点等是否存在特殊情况。带头双向循环链表,它的代码实现简洁明了,这正是他的结构优势。

二、各个接口函数的实现:

结构体类型定义以及头节点的创建:

ListNode* plist=NULL;   //创建头节点
typedef struct ListNode ListNode;

typedef int ListDataType;

struct ListNode
{
	ListDataType val;
	ListNode* pre;
	ListNode* next;
};

双向链表最常见的包括这些接口:(先介绍)

//增容 开辟新节点

ListNode* ListMalloc(ListDataType x);


//初始化头结点。

ListNode* ListInit();


//头插

void PushFront(ListNode* phead);


//尾插

void PushBack(ListNode* phead);


//头删

void PopFront(ListNode* phead);


//修改节点。

void ListModify(ListNode* pos,ListDataType x);


//查找节点

ListNode* ListFind(ListNode* phead);


//在任意一个节点前插入

void ListInsert(ListNode* phead,ListDataType);


//打印

void ListPrint(ListNode* phead);

☁️增容–开辟新节点

//增容 开辟新节点
ListNode* ListMalloc(ListDataType x)
{
	ListNode* str = (ListNode*)malloc(sizeof(ListNode));
	str->val = x;
	str->pre = NULL;
	str->next = NULL;
}

☁️初始化头节点

初始化头节点,或者说是"初始化头指针" 更为恰当,是将头指针指向一个节点,

该节点就称为头节点。

头结点又叫哨兵位,不做数据存储使用,而是为了操作方便,当链表中存在哨兵位时,链表的有效数据是从第二个节点开始的。

需要注意的是:既然初始化头结点需要改变头指针的指向,要改变指针中存储的地址,有多种方法:

①方法 1

以返回值的形式让头指针plist接收,通过给plist赋值的形式改变plist的指向。

//初始化头节点 无->有

ListNode* ListInit()
{
	ListNode* phead = ListMalloc(0);
	phead->next = phead;
	phead->pre = phead;
}
②方法 2

在函数形参中定义二级指针用于接收头指针plist的地址。

通过访问plist的地址去改变plist所存储的内容——地址。

两种方法都可以,视个人习惯而定。

//二级指针
void ListInit(ListNode** phead)
{
	*phead = ListMalloc(0);
	(*phead)->pre = *phead;
	(*phead)->next = *phead;

}

☁️头插

在这里插入图片描述

头插时可以加个断言避免phead为空的情况。

//头插

void PushFront(ListNode* phead,ListDataType x)
{
	assert(phead);
	ListNode* next = phead->next;//哨兵位的下一个节点才是第一个有效节点
	ListNode* data = ListMalloc(x);//开辟一个新节点,用于头插
	data->pre = phead;
	data->next = next;
	phead->next = data;
	next->pre = data;
	
}

☁️尾插

在这里插入图片描述

尾插也加一个断言。

void PushBack(ListNode* phead,ListDataType x)
{
	assert(phead);
	ListNode* tail = phead->pre;//最后一个节点的位置
	ListNode* data = ListMalloc(x);//开辟一个新节点,用于尾插
	tail->next = data;
	data->pre = tail;
	data->next = phead;
	phead->pre = data;
	
}

☁️头删

头删要注意的是:

当链表只剩一个头节点的时候,不能再继续删除了,若是把头节点也删了,并且置为空指针,链表将不复存在。

因此我们不仅要断言判断头节点是否为空,还要判断头节点的指向后继的指针是否指向了自己,如果是,就证明该链表中只剩头节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V8J8Z9Qa-1639289730818)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20211209180302442.png)]

//头删

void PopFront(ListNode* phead)
{
	
	assert(phead);
	assert(phead->next!=phead);  //只剩头节点了,不能把头节点给删了
	ListNode* first = phead->next;  //第一个节点
	ListNode* second = first->next; //第二个节点
	
	free(first);
	phead->next = second;
	second->pre = phead;

	first = NULL;
}

☁️尾删

尾删同样也要断言头节点不能被删。

//尾删

void PopBack(ListNode* phead)
{
	assert(phead);
	assert(phead->next!=phead);  //只剩头节点了,不能把头节点给删了
	ListNode* First = phead->pre;//最后一个节点的位置
	ListNode* Second = First->pre;//
	phead->pre = Second;
	Second->next = phead;
	
	free(First);
	First = NULL;

}

☁️修改节点数据

//修改节点数据

void ListModify(ListNode* pos,ListDataType x)
{
	assert(pos);
	pos->val = x;
}

☁️查找节点

ListNode* ListFind(ListNode* phead,ListDataType x)
{
	//从第一个有效节点开始查找
	ListNode* cur = phead->next;
	while (cur != phead)  //循环链表走到最后的依据不是NULL,而是头节点点
	{
		if (cur->val == x)
		{
			return cur; //找到了,返回该节点地址
		}
		cur = cur->next;
	}
	return NULL; //没找到,返回NULL

}

☁️在任意节点前插入,单链表如何?

/*
if (pos)
{
	ListModify(pos,8);
}
*/

//先找节点,有这个节点再进行插入。

void ListInsert(ListNode* pos, ListDataType x)
{
	assert(pos);
	assert(pos != pos->next);
	ListNode* pre = pos->pre; //马上就找到了pos的前一个节点
	ListNode* newnode = ListMalloc(x);
	pre->next = newnode;
	pos->pre = newnode;
	newnode->pre = pre;
	newnode->next = pos;
	
}

单链表当然也可以实现在 某一个节点(假设A节点)前插入节点,但是因为它是单向的,无法记住前一个节点的地址,因此在找到A节点后,我们需要重新再从第一个有效节点开始,遍历链表,直到找到A节点的前一个节点,时间复杂度为O(n)。

而如果在双向链表的环境下,只要找到A节点,A节点立刻就可以找到它的前一个节点。

☁️删除任意节点

//删除任意节点

void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* First = pos->pre;
	ListNode* Second = pos->next;
	First->next = Second;
	Second->pre = First;
	free(pos);
}

☁️打印链表

//打印

void ListPrint(ListNode* phead)
{

	ListNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->val);
		cur = cur->next;
	}
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿波呲der

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值