双向带头循环链表

链表实际上有八种组合出来的结构:
1.单向或者双向
在这里插入图片描述
在这里插入图片描述
2.带头或者不带头
在这里插入图片描述
在这里插入图片描述
3.循环或者非循环
在这里插入图片描述
在这里插入图片描述

今天来讲一下双向带头循环链表的一些相关知识点以及代码整理:

双向带头循环链表的特点以及实现

双向带头链表的最大的特点就是复杂,没错,它是所有链表中结构最为复杂的一种链表,但是使用起来确实最简单的。

结构体的创建以及链表的初始化

typedef int DLDatatype;
typedef struct DListNode
{
	struct DListNode* prev;
	struct DListNode* next;
	DLDatatype data;
}DLTNode;

那么如何对链表进行初始化呢?

这里要注意一个问题,形参是实参的拷贝,如果函数无返回值,那么需要传递一个二级指针,如果有返回值,那么需要将返回值返回。

DLTNode* BuyDListNode(DLDatatype x)
{
	DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
	if (node == NULL)
	{
		perror("malloc::BuyDListNode");
		exit(-1);
	}
	node->data = x;
	node->next = node->prev = NULL;
	return node;
}
void DlistInit(DLTNode** phead)
{
	*phead = BuyDListNode(-1);
	(* phead)->next = *phead;
	(* phead)->prev = *phead;
}
LTNode* BuyListNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc::newnode");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
//初始化链表
LTNode* ListInit(LTNode* phead)
{
	phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

链表的尾插

在这里插入图片描述

尾插不需要像单链表一样寻找尾结点,头结点的prev指针所指向的内容就是尾结点。同样,尾结点的next指向头指针。

//尾插
void DListPushBack(DLTNode** phead, DLDatatype x)
{
	assert(phead);
	DLTNode*newnode=BuyDListNode(x);
	DLTNode* last = (* phead)->prev;
	last->next = newnode;
	newnode->next = *phead;
	newnode->prev = last;
	(*phead)->prev = newnode;
}

这里要注意一个问题,链表是循环的,一定注意头结点的prev指针所指向内容的修改。

链表的头插

和尾插相似,但是要注意这是带头的双向循环链表,phead是一个哨兵位的头结点,千万不要把新开辟的结点插入到phead之前。
头插需要再phead之后插入内容,那么就意味着需要创建一个指针来保存头插前phead->next所指向的内容。

//头插
void DListPushFront(DLTNode** phead,DLDatatype x)
{
	assert(phead);
	DLTNode*newnode=BuyDListNode(x);
	DLTNode* node = (*phead)->next;
	(*phead)->next = newnode;
	newnode->next = node;
	newnode->prev = *phead;
	node->prev = newnode;
}

链表的尾删

链表的尾删除只需要修改被删除部分的prev和next指针所指向的内容,然后将删除元素free掉即可。

//尾删
void DListPopBack(DLTNode** phead)
{
	assert(phead);
	DLTNode* pop = (*phead)->prev;
	//找到被pop的元素
	DLTNode* node = pop->prev;
	node->next = *phead;
	(*phead)->prev = node;
	free(pop);
}

链表的头删

链表头删要删除phead后的一个元素,直接将其pop,pop之前记录被删除元素所指向的下一个元素的地址。

//头删
void DListPopFront(DLTNode** phead)
{
	assert(phead);
	DLTNode* pop = (*phead)->next;
	DLTNode* node = pop->next;
	(*phead)->next = node;
	node->prev = *phead;
	free(pop);
}

判断是否为空链表

在这里插入图片描述

这里的Data是一个无效的数据,这个哨兵位的头结点在链表为空时next指针指向本身这个结点,prev指针也是指向这个结点。所以判断也很简单。

//判断链表是否为空
bool DListEmpty(DLTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

在pos位置之前插入x

在这里插入图片描述

如图所示,加入pos为D2所在的位置,那么在pos之前插入X,就要处理好D1与X,X与D2之间的关系。

//在pos位置之前插入X
void DListInsert(DLTNode** pos, DLDatatype x)
{
	assert(pos);
	DLTNode*newnode=BuyDListNode(x);
	DLTNode* prev = (*pos)->prev;//找到pos位置之前的元素
	prev->next = newnode;
	newnode->prev = prev;
	//先将prev和newnode两个结点连起来
	newnode->next = *pos;
	(*pos)->prev = newnode;
	//将pos和newnode这两个结点连接起来
}

删除pos位置的结点

删除pos位置的结点,需要将pos位置之前和之后的结点连在一起。所以代码也很简单。

//删除pos位置的结点
void DListErase(DLTNode** pos)
{
	assert(pos);
	//先记录pos位置之前和之后的位置
	DLTNode* nextnode = (*pos)->next;
	DLTNode* prevnode = (*pos)->prev;
	nextnode->prev = prevnode;
	prevnode->next = nextnode;
	free(pos);
}

求链表的长度

求链表的长度不能将链表的哨兵位的元素计算入内。

//求链表的长度
int DListSize(DLTNode** phead)
{
	int size = 1;
	DLTNode* node = (*phead)->next;
	while (node->next != (*phead))
	{
		node = node->next;
		size++;
	}
	return size;
}

链表的销毁

双向链表想要销毁,需要将每个结点逐一进行销毁。不可以直接对头结点进行销毁后就认为销毁完毕。这里注意一点,在销毁掉一个结点之前要先保存下一个结点

//链表的销毁
void DListDestroy(DLTNode** phead)
{
	assert(phead);
	DLTNode* node = (*phead);
	while (node->next != (*phead))
	{
		DLTNode* next = node->next;
		DListErase(&node);
		node = next;
	}
	node = NULL;
}

链表和顺序表的对比

链表和顺序表各有其优缺点,针对其优缺点,有如下整理

顺序表的优点:
顺序表可以通过下标随机访问表中的元素,CPU高速缓存命中率高。

顺序表的缺点:
通过对顺序表的了解,头部或中间删除或插入效率低,扩容有一定的性能消耗,可能存在一定程度的空间浪费

这里就提一个问题:为什么顺序表的扩容一般都是以1.5或2倍进行扩容?
首先要明白顺序表的存储是在一块连续的空间,所以在添加元素的过程中,如果之前的空间存放不下,就会重新开辟一块新的空间,把数据复制到新开辟的内存中,并释放掉之前的内存。但是在复制的这个过程中,时间复杂度为O(n),所以一次扩容尽可能开辟得大一点,减少复制的过程。

链表的优点(以带头循环的双向链表为例子):
任意位置的插入删除的时间复杂度都是O(1),按需申请释放空间。

链表的缺点:
链表不支持下标的随机访问。

至此带头的双向循环链表就结束了,带头循环的双向链表是最为复杂的一种链表结构,有问题评论区见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Feng,

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

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

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

打赏作者

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

抵扣说明:

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

余额充值